CIS 1951
Fall 2024

HW4: Weather App

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

Introduction

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.

Requirements

Your app should have at least three screens:

  1. A home screen that lets the user enter a location with some combination of text fields.
    • This can be one text field for a general address/location string, or two to enter a city and state, or any combination of the above. We will leave this to you to decide how you want to ask the user for the location.
    • This cannot be a longitude and latitude input field, as will be explained later.
    • Must include some "submit" button which will bring the user to the next page (screen #2) with the weather for that location.
  2. A screen showing the current weather of a given location when opened (similar to the normal iOS weather app) (this should query the weather API when opened). We will refer to this as the "location detail" screen from now on. This will include two save buttons:
    • One to save/favorite the location searched (or unsave/unfavorite if already saved)
      • This option just saves/favorites the location itself without any associated weather data. For example, if a user searches for "Philadelphia, PA", they can favorite this location to quickly retrieve current weather there later
    • One to save the weather at that time
      • This option saves the weather data (temperature, conditions, etc.) at the moment the location was searched. This creates a "snapshot" of the weather at that specific time for later reference.
  3. A screen showing a previously saved weather snapshot. We will refer to this as the "weather snapshot" screen from now on.

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
    • In this implementation, the left sidebar (of the split view) could have two sections (two lists): One would have the favorited locations, and one would have the saved weather snapshots.
    • Then you could have a single option at the very top of the sidebar to search for a new location
  • A TabView with the .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) modifier
    • We haven't discussed this before, but this allows you to swipe between tabs to navigate between different views.
    • In this implementation, you could swipe to navigate between favorited locations, and maybe have a separate button to open saved weather snapshots.

Specifically, the location detail screen (screen #2) should:

  1. Show the location that the weather was queried at
    • This should be the returned value from the geocoding API explained below, NOT the user's input from the last screen.
  2. Show the weather at the location specified
    • The weather API provides a ton of options and optional query parameters, but you MUST show the following (feel free to add more if you wish!):
      • Temperature (temperature_2m)
      • Precipitation probability (precipitation_probability)
      • Precipitation (precipitation)
    • You MUST use a CodingKeys enum to clean up these names in the returned JSON for the Decodable struct.
  3. A button to favorite/unfavorite this search location so that it is added to the favorites list.
  4. A button to save the weather snapshot of this location. It should include a timestamp (a Date) for easy identification (and the location of course).

Saving Data

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.

API Documentation / Help

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.

  1. Geocoding API

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.

  1. Weather API

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.

Instructions

Some brief instructions to get you started:

Step 1: Plan your app

Start by reading through these instructions and planning out how you're going to structure your app and its screens.

In particular, think through:

  • What models, view models, and views do you need?
  • How will your view model communicate with its views?
  • Which part of your architecture will handle fetching the API data?
  • How and where will you keep track of favorite locations?
  • How will you let the user navigate to these favorited locations, and the saved weather snapshots?

Step 2: Investigate the APIs mentioned and make models for returned results

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.

Step 3: Create a view model to store favorited locations and saved weather snapshots

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.

Step 4: Write the home screen

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.

Step 5: Make file for API calls

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.

Step 6: Write the location detail screen

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).

Step 7: Write the weather snapshot screen

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.

Step 8: Finish the NavigationSplitView on the home screen

Add two lists to the left side bar with the list of favorited locations and saved weather snapshots.

Step 9: Test and submit!

Be sure to test using multiple different locations, as well as closing and reopening the app for data persistence!

Resources

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:

Grading

This assignment is worth 100 points, broken down as follows:

Home and Weather Views (25 points)

  • 5 points: Home screen displays a list of favorited locations and saved weather snapshots
  • 5 points: Home screen has a field or fields for user input, and a button to submit
  • 5 points: Location detail screen shows the location name FROM THE GEOCODING API, not the user input, and at least the 3 required fields
  • 5 points: Location detail screen has two buttons, one to favorite/unfavorite the current location, and one to save the weather snapshot
  • 5 points: Weather snapshot screen shows the location, weather, and timestamp of the snapshot, and has a button to delete the snapshot

Networking & API Calls (25 points)

  • 5 points: App correctly uses Geocoding API
  • 5 points: App correctly decodes result of Geocoding API
  • 5 points: App correctly uses Weather API
  • 5 points: App correctly decodes result of Weather API
  • 5 points: App calls these API functions at the appropriate locations

Persistent Data Storage (25 points)

  • 5 points: Decided on appropriate types of persistent storage for data
  • There are 2 places where data needs to be stored, and each is worth 10 points:
    • If UserDefaults is used:
      • 10 points: Correctly uses @AppStorage wrapper on variable and variable is properly used in the code
    • Anything else:
      • 5 points: App correctly sets up whichever framework was chosen
      • 5 points: App correctly sets uses whichever framework was chosen, and writes/reads data from it correctly

App Architecture (15 points)

  • 5 points: App uses a model struct to represent the locations and weather data
  • 5 points: App uses a view model to keep track of the data used throughout the app
  • 5 points: App successfully uses @StateObject, @ObservedObject, and/or @EnvironmentObject to share the view model between different screens

Code Quality (10 points)

  • 10 points: Code is readable (i.e. indentation and naming of symbols is reasonable)

Deductions

  • -5 points: Xcode project contains a broken file reference preventing compilation
  • -20 points: App doesn't compile

General bugs (those not covered in the above deductions) will be subject to these deductions:

  • -2 points: Minor bug, e.g. a UI/UX issue that appears in limited circumstances and doesn't affect the core flow
  • -5 points: Major bug, e.g. a bug that prevents the core flow of the app but can be worked around in-app
  • -10 points: Fatal bug, e.g. a bug that requires code modification to continue testing the core flow
    • Crashes on launch, or during major parts of the app flow, will fall into this category

Bugs encountered when testing extra credits will be deducted from their respective extra credit points.

Extra Credits

  • Current location (+5) Add a way to autofill the user's current location into the input fields (see Lecture 7 for details on getting location!)
  • Map (+5): Show the given location in a map in the location detail view.

Submission

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 →
Dates and times are displayed in EST.