Submit this assignment on Canvas.
Author: Jordan Hochman
Please leave feedback by posting on Ed or contacting the course staff.
Required Software: macOS Ventura, Xcode 15
Deadline: Monday, 4/8 @ 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 historical 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 you add different saved locations such that when pulling up those saved locations, you will be able to see the weather fetched from them (similar to the actual Weather app on the iPhone).
Your app should have at least three screens:
Therefore you will need some way to save any given search, or the results from that search (probably two buttons on the weather detail screen). Then you will need a way to navigate to these saved search locations, and a way to see prior saved results. You need to decide how it's best to navigate between these, but we recommend one of the following:
NavigationSplitView
similar to what we had with the Minicourse Browser and iOStagram where saved locations would appear on the side bar. The left sidebar could be split into two sections (two lists), one on top showing the saved locations, and one on the bottom showing the saved results.TabView
with the .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
modifier. Then you can swipe to navigate between saved locations, if they're there. Then you can decide how else to navigate to the saved search results.Specifically, the weather detail screen should:
There are two parts to saved data.
The first thing to save is the saved locations that can be reponened/requeried in the future. This can/should be a list of longitude, latitude pairs (which will uniquely identify a locaiton for us) (this is returned by the geocoding API).
The second thing to save is the weather results from a given query. These saved results must include the 3 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. 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 documentaiton, 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 fact that the API allows us to use either method 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 qery parameters matches 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 probably take the first result given for your results. 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. Everything else you don't need to save so it doesn't need to be in the decodable struct.
There are tons of query parameters able to 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 predition 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 gotten 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 a 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 probably 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 you can omit fields in a decodable struct, and even if they're in the returned json they won't be decoded which is useful.
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 will probably also want to store the time list so you can later reference the current time for the prediction. You should also probably add a field to the decodable struct that represents the timestamp it was made, using Date() (so it's easier to save the result later).
Here's a starting point for your structs, but 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 probably 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 saved locations), and an array of saved search results (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 similar to that (this will be directly passed into the geocoding API later). Then make a button that will get the location (longitude/latitude, the Location struct) from this string, and move to the weather details view from the resulting Location struct.
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 probably only want to return the first one (but also note that there may be none returned at all!).
You will also want to make a function to get the weather given an input longitude and latitude (doubles) 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 an @State
variable to this data. 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 either save the location, or the location and it's associated timestamped weather data. 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 saves the location), if the location has already been saved this should become an unsave button (which removes it from saved locations).
This screen will take in the Location and a WeatherInfo (or something similar if you changed the structs somehow). It should then show the data from these, as well as the timestamp. There should also be a button to delete the saved search result on this screen, which then navigates you back to home (after deletion).
Add two lists to the left side bar with the list of saved locations, and saved results.
Be sure to test using multiple different locations, as well as closing and reponening 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 the home and dining hall screensTo submit, upload a zip (or .tar.gz) of your entire Xcode project to Canvas. Make sure you've given the requirements another read before you do so.
Submit on Canvas →