Submit this assignment on Gradescope.
Author: Jordan Hochman
Please leave feedback by posting on Ed or contacting the course staff.
Required Software: macOS Ventura, Xcode 15
Deadline: Thursday, 11/21 @ 11:59 PM
In this assignment, you will be building a weather app! This weather app will allow you to enter in locations and get the weather at those locations, as well as save previous searches and results.
You will use two APIs for this, the first is Open Meteo's free weather API and Nominatim's geocoding API (to get longitude/latitude from city/address). The app will let favorite different locations so you can easily see the weather at those locations (similar to the actual Weather app on the iPhone).
After the user searches for a location, the geocoding API will give the exact longitude and latitude, and then the weather API will give the weather at this longitude and latitude.
Your app should have at least three screens:
Therefore, at a high level, you will need some way to favorite the given location, and a way to save the weather snapshot at that time.
Then, you will need a way to navigate to the location detail screen (for either a searched location or a favorited location), and a way to navigate to the weather snapshot screen (for a saved weather snapshot). You can decide how to navigate between these, but we recommend one of the following options:
NavigationSplitView
similar to what we had with the Minicourse Browser and iOstagram
TabView
with the .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
modifier
Specifically, the location detail screen (screen #2) should:
temperature_2m
)precipitation_probability
)precipitation
)CodingKeys
enum to clean up these names in the returned JSON for the Decodable struct.Date
) for easy identification (and the location of course).There are two parts to saved data as explained above.
The first thing to save is the favorited locations that can be reopened/re-queried in the future. This should be a list of longitude, latitude pairs which will uniquely identify a location for us (this is returned by the geocoding API) (alternatively you can use a list of Location
structs mentioned below).
The second thing to save is the weather snapshot for a given location. These saved snapshots must include the 3 weather fields mentioned above, as well as the location (longitude, latitude) and a timestamp (Date
) from when this query was made.
Both of these must be saved to the device persistently. You will need to decide which method to use to save these to the device. While you may use UserDefaults
for one of these, you MUST use at least 1 other appropriate framework to store some of the data.
As both APIs have a lot of documentation, we will give a brief rundown of them here, however please feel free to read through them yourself.
We will be using the search
route which takes in many optional query parameters. Reading through the provided documentation, you can either provided a q
query parameter with any input string, or query parameters like street
, city
, county
, state
, country
. The API allows us to use either method which is what gives us the option of allowing the user to either input a specific city/state on the main view, or a generic input string. Thus you will need to use whichever query parameters match your desired implementation.
Note that you need to include the query parameter format=json
in order to get the json response data. To investigate this API, we recommend either using manual curl
requests, or using Postman if you have experience with that. Here is an example query using curl if you want to test in terminal:
curl "https://nominatim.openstreetmap.org/search?q=philadelphia&addressdetails=1&format=json"
We recommend using this JSON Formatter to view the returned JSON in a readable format. For the example above, this gives the following (some of it was removed for brevity):
[
{
"place_id": 14128726,
"licence": "Data © OpenStreetMap contributors, ODbL 1.0. http://osm.org/copyright",
"osm_type": "relation",
"osm_id": 188022,
"lat": "39.9527237",
"lon": "-75.1635262",
"class": "boundary",
"type": "administrative",
"place_rank": 16,
"importance": 0.7137973339835988,
"addresstype": "city",
"name": "Philadelphia",
"display_name": "Philadelphia, Philadelphia County, Pennsylvania, United States",
"address": {
"city": "Philadelphia",
"county": "Philadelphia County",
"state": "Pennsylvania",
"ISO3166-2-lvl4": "US-PA",
"country": "United States",
"country_code": "us"
},
"boundingbox": [
"39.8670050",
"40.1379593",
"-75.2802977",
"-74.9558314"
]
},
{
"place_id": 376330130,
...
},
...
]
Note that it gives multiple locations that all could match your input, so you should take the first result given. Note that while we especially care about the longitude and latitude of this result (to use in the weather API), this also provides us with useful location data as well (e.g. like city/state) if you use the addressdetails=1
query parameter, so you should save that too. You don't need to save anything else, so it doesn't need to be in the decodable struct.
There are tons of query parameters that can be used for this, but you will need to figure out which ones you need and how to use them. Note that it requires a longitude and latitude, not some user typed address. This is why we need the previous Geocoding API. Also note that there are query parameters that represent the units of the results, which might be useful for temperature. To give an example of one possible query URL:
curl "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&hourly=temperature_2m,precipitation_probability,precipitation&temperature_unit=fahrenheit&forecast_days=1"
Note that this is giving hourly data, and there are many options as to how to configure the prediction range. For our app, we only care about the current weather for the requirements, but feel free to save all of the data or give input options for forecast range if you wish.
Here is the example response for that query (again some parts omitted for brevity):
{
"latitude": 52.52,
"longitude": 13.419998,
"generationtime_ms": 0.08404254913330078,
"utc_offset_seconds": 0,
"timezone": "GMT",
"timezone_abbreviation": "GMT",
"elevation": 38,
"hourly_units": {
"time": "iso8601",
"temperature_2m": "°F",
"precipitation_probability": "%",
"precipitation": "mm"
},
"hourly": {
"time": [
"2024-03-25T00:00",
...,
"2024-03-25T23:00"
],
"temperature_2m": [
41.3,
...,
37.9
],
"precipitation_probability": [
97,
...,
0
],
"precipitation": [
0.3,
...,
0
]
}
}
Note a couple things: First, the output longitude and latitude are NOT the same as the queried ones. This is because the weather can't necessarily be fetched at every location. You should ignore these output coordinates, and instead use the input ones as the identifier for a location if needed.
Secondly, depending on what optional query parameters you use for your query (and the length of the forecast predictions), the output data is in a big list of hourly predictions. You only need to get the current hour, which can be found using the appropriate index in the time
field above.
Some brief instructions to get you started:
Start by reading through these instructions and planning out how you're going to structure your app and its screens.
In particular, think through:
When you're ready, create a new SwiftUI project. You'll most likely want to start by investigating the return type from the APIs, and make structs for the returned data.
For the Geocoding API, we care about the longitude and latitude, as this will identify the location if it's saved for later. But we also want to save the address information so we can show the detailed information about it on the weather screen. Implement a Decodable struct that represents this location data you want returned. Note that if some fields of the json are omitted in the corresponding decodable struct, they just won't be included in the decoded result.
Now make a struct for the returned weather results. You must include at least the 3 fields mentioned above, and you must use CodingKeys
to remove the _2m
from the json field names that end in it. You should also store the time list so you can later reference the current time for the prediction. Also add a field to the decodable struct that represents the timestamp it was made using Date()
(so it's easier to save the weather snapshot).
Here's a starting point for your structs, although you will probably need to edit them:
struct Location: Identifiable, Decodable {
let lat: Double
let lon: Double
let name: String
let display_name: String
let address: Address
var id: String { "\(lat)_\(lon)" }
}
struct Address: Decodable {
let city: String?
let county: String?
let state: String
let country: String
let country_code: String
}
struct WeatherInfo: Decodable {
let timestamp = Date()
let hourly_units: ???
let data: WeatherData
enum CodingKeys: String, CodingKey {
case hourly_units
case data = "hourly"
}
}
struct WeatherData: Decodable {
let time: [Date]
let temperature: [Double]
let precipitation_probability: [Int]
let precipitation: [Double]
enum CodingKeys: String, CodingKey {
case time
case temperature = "temperature_2m"
case precipitation_probability
case precipitation
}
}
Note: For decoding the dates properly, you will need to set the JSON decoder's dateDecodingStrategy
to a custom value. Look up online how to set this so the dates are decoded properly.
Once you've made the structs above, you'll want to create an array of locations (representing the favorited locations), and an array of saved weather snapshots (or maybe a dictionary... it's up to you to decide since you will need some way of identifying/matching up the saved weather data result with its location. You could also edit the structs above to combine them in some way if you wish.).
These will also need to be saved to the device, so you will need to make appropriate modifications so that each of these are saved when updated in the view model. This depends on what you decide to use for each one.
Useful tip: If you are using UserDefaults
and have an @AppStorage
property wrapper inside some ObservableObject
, this automatically makes the variable also act as @Published
.
Now use @StateObject
in your main view to create an instance of this view model for your app. Then, use the .environmentObject
modifier to pass it to all other views, much like we did in lecture 5.
Now, you can start writing the home screen, using the view model and models you wrote in steps 2 and 3. As mentioned earlier, one possible option for the navigation is the NavigationSplitView
(like in Minicourse Browser and iOstagram).
But for now, just worry about the content of the home view. We want a few input boxes, but the details are up to you. Decide if you want the user to either enter a generic string that could represent a location, or if you want multiple boxes for city/state, or something else (this will be directly passed into the geocoding API later). Then make a button that will fetch the location from the geocoding API (returning the Location
struct), and move to the location detail screen for this resulting Location
.
Similar to what we did in lecture 8, make a file that will handle all of your API calls.
You probably want to start with a getLocation
function. This should take in as inputs whatever the user input (e.g. either a generic string, two strings for city/state, or something else depending on what you decided in step 4). Then it should call the appropriate geocoding URL to decode the response data and return the result. Note that it returns a list of locations, so you should only return the first one (but also note that there may be none returned at all!).
Then make a function to get the weather given an input longitude and latitude (of type Double) using the weather API. You can alternatively make it take in a Location
struct. This should return the WeatherInfo
struct you made above.
This screen will take in a Location
(containing longitude and latitude). Then in a .onAppear
modifier, call the appropriate API function you made to get the weather data for that location, and set a @State
variable to this data (or something in the view model). This should cause the view to display the proper results. You MUST show the 3 fields mentioned above, in addition to some sort of description of the location (like city/state or address).
Also add two buttons to favorite/unfavorite the location and save the weather snapshot. The exact implementation details will depend on which data persistence you used, but they will probably call functions in your view model.
For the first button (which favorites the location), if the location has already been favorited, then it should unfavorite the location (which removes it from favorited locations).
This screen will take in the Location
and a WeatherInfo
(or something similar if you changed the structs above). It should then show the data from these as well as the timestamp (just as a formatted date). There should also be a button to delete the saved weather snapshot on this screen, which then navigates back to home after deletion.
NavigationSplitView
on the home screenAdd two lists to the left side bar with the list of favorited locations and saved weather snapshots.
Be sure to test using multiple different locations, as well as closing and reopening the app for data persistence!
This is a fairly complex assignment. We encourage you to come to office hours or ask on Ed if you have any questions or are running into trouble. We're here to help!
Some relevant course material:
This assignment is worth 100 points, broken down as follows:
@AppStorage
wrapper on variable and variable is properly used in the code@StateObject
, @ObservedObject
, and/or @EnvironmentObject
to share the view model between different screensGeneral bugs (those not covered in the above deductions) will be subject to these deductions:
Bugs encountered when testing extra credits will be deducted from their respective extra credit points.
To submit, upload a zip (or .tar.gz) of your entire Xcode project to Gradescope. Make sure you've given the requirements another read before you do so.
Submit on Gradescope →