CIS 1951
Spring 2025

HW4: Weather App

Please leave feedback by posting on Ed or contacting the course staff.

Required Software: macOS Ventura, Xcode 15

Deadline: Wednesday, 4/9 @ 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 previously searched locations.

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 two 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 (You will need to use the geocoding API to get this data.)
    • Must include some "submit" button which will bring the user to the next page (screen #2) with the weather for that location.
  2. A "location detail" screen showing the current weather of a given location when opened (similar to the normal iOS weather app).
    • Must show the location that the weather was queried at
      • This should be the returned location information from the geocoding API (more details below), NOT the user's input from the home screen.
    • Should query the weather API when opened and show the weather at the location
      • The weather API provides many 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 (example below).
    • Must have a button to favorite/unfavorite this location so that it is added to the favorites list.
      • This option saves the location (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

Also, you will also need some way to see previously saved locations, and a way to navigate between all the screens. We leave the design details of this up to you, but for some examples, you could:

  • Add the saved list on (at the bottom of) the home screen
  • Add the saved list on a new screen, linked from the home screen (potentially with a ToolbarItem)
  • Use a NavigationSplitView similar to what we had with the Minicourse Browser and iOstagram
    • In this implementation, the left sidebar (of the split view) could have the list of favorited locations
    • Then you could have a single option at the very top of the sidebar for the home screen
  • Use a TabView with the .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) modifier
    • We haven't discussed this before, but it allows you to swipe between tabs to navigate between different views.
    • In this implementation, you could swipe to navigate between the home screen and screens for each of the favorited locations

Saving Data

You will need to save the favorited locations so they can be reopened/re-queried in the future. You can make the struct to represent a location however you wish, but it MUST include the longitude and latitude fetched from the geocoding API. These together will uniquely identify a location. We provide a sample Location struct below.

This must be saved to the device persistently. You will need to decide which method to use to save these to the device, but you may not use UserDefaults. Whatever option you pick must be an appropriate framework to store 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 unique identifier for a location.

Second, 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?

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.

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 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. We also recommend using convertFromSnakeCase.

Step 3: Create a view model to store favorited locations

Once you've made the structs above, you'll want to create an array of locations (representing the favorited locations).

These will also need to be saved to the device, so you will need to make appropriate modifications so that they are saved when updated in the view model. This depends on what type of persistent storage you decide to use.

Now use @State in your main view to create an instance of this @Observable view model for your app. Then, use the .environment 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 a button to favorite/unfavorite the location. The exact implementation details will depend on which data persistence you used, but they will probably call functions in your view model.

If the location has already been favorited, then the button should unfavorite the location (which removes it from favorited locations list).

Step 7: Finish the NavigationSplitView on the home screen (if applicable)

Add a list to the left side bar with the list of favorited locations.

Step 8: 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

Note: This rubric is still undergoing a few changes. It'll be finalized by the end of this week.

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

Home and Weather Views (25 points)

  • 5 points: The list of favorited locations is displayed somewhere (on the home screen or elsewhere)
  • 5 points: Navigation between screens is easy to use and works correctly
  • 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 a button to favorite/unfavorite the current location

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 type of persistent storage for data
  • 20 points: Correct use of persistent storage (UserDefaults not allowed)
    • 10 points: App correctly sets up whichever framework was chosen
    • 10 points: App correctly 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 @State and/or @Environment 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.