WeatherKit: Build a Simple iOS Weather App

ยท

6 min read

WeatherKit: Build a Simple iOS Weather App

Table of contents

No heading

No headings in the article.

WeatherKit framework was one of the amazing Apple announcements during WWDC22.

In this article, we'll create a simple iOS weather app that shows the current weather by using the WeatherKit and SwiftUI frameworks. Also, some additional information will be provided regarding the framework.

But before we start, you should know that there are some requirements you should meet:

  • Apple Developer Program membership: for accessing WeatherKit's REST APIs by registering the app's bundle ID.
  • Xcode 14: due to the API availability for iOS/iPadOS 16 SDKs and higher.

Additionally, you should consider these requirements by Apple if you're planning to use it in your app(s) in production:

  • It's not fully free, only 500,000 API calls are comes for free with your membership per month. You should pay if you want to get more. Here's the pricing: 500,000 calls/month: Included with membership. 1 million calls/month: US$ 49.99. 2 million calls/month: US$ 99.99. 5 million calls/month: US$ 249.99. 10 million calls/month: US$ 499.99. 20 million calls/month: US$ 999.99.
  • You must put the Apple Weather attribute in your app.

If you're all set, let's start. ๐Ÿš€

After you set up the project on Xcode by selecting SwiftUI as the interface, you need to head over to the Certificates, Identifiers & Profiles section in App Store Connect, click on Identifiers and register your app's bundle ID. In the top left, click the add button (+), select App IDs, then click Continue. After that, register your app ID by enabling the WeatherKit checkbox in the Capabilities tab.

Screenshot 2022-07-23 at 12.39.43 AM.png

You can go back and select the Services tab if you want to manage your WeatherKit usage.

Screenshot 2022-07-23 at 12.42.47 AM.png

Now, get back to Xcode. Select the xcodeproj file in the Project Navigator and click on the Targets tab. In the top-left of that tab, click on the Capability and add WeatherKit.

Screenshot 2022-07-23 at 12.46.06 AM.png

That was all for the configuration, let's build the app.

As you see on the Project Navigator in Xcode, we only have the ContentView file which is the app's main view and is empty, let's leave it as it is for now.

Before dealing with WeatherKit, we need to get the user's current location to retrieve the weather information. Hit โŒ˜+N on your keyboard and create a new file named LocationManager to create a class for the purpose. We basically ask for the user's location permission there. And then, we'll get the last updated location.

import CoreLocation

public class LocationManager: NSObject,
                              CLLocationManagerDelegate,
                              ObservableObject {

    static let shared = LocationManager()

    let locationManager = CLLocationManager()
    @Published var lastLocation: CLLocation?

    override init() {
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
    }
}

extension LocationManager {
    public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.first else {
            return
        }

        lastLocation = location
        locationManager.stopUpdatingLocation()
    }
}

There's one more step we need to take for asking the user to get his/her location permission, and it's by adding a key in info.plist. Click on the project file, your target, select Info tap, and then add this key with a meaningful description:

Privacy - Location Usage Description

We need a model with a view model. Simply create a new file by hitting โŒ˜+N on your keyboard, name it Weather. And then, type import WeatherKit at the top of the file.

For the model, we call it Weather. Here are the properties that we'll be going to have inside it:

  • temperature: current weather temperature.
  • condition: current weather condition text.
  • symbolName: an SFSymbol icon based on the weather condition.
  • humidity: current weather temperature humidity rate.
  • isDaylight: knowing whether it's daylight or not.
struct Weather {
    let temperature: Double
    let condition: String
    let symbolName: String
    let humidity: Double
    let isDaylight: Bool

    static func empty() -> Weather {
        Weather(temperature: 0,
                condition: "",
                symbolName: "",
                humidity: 0,
                isDaylight: false)
    }
}

Now, time to create the view model. Below the model, we're creating a class called WeatherViewModel and conforming it to ObservableObject.

class WeatherViewModel: ObservableObject {
}

We mainly use the WeatherService that comes with WeatherKit , so let's make an instance of it inside the class.

class WeatherViewModel: ObservableObject {
  let service = WeatherService()
}

We also need another instance for the LocationManager class we created before, and a published property for saving the weather data we're fetching to use it on the view. Our class should look like this:

class WeatherViewModel: ObservableObject {

    let service = WeatherService()
    let locationManager = LocationManager()

    @Published var currentWeather: Weather = .empty()
}

Now, below the instances and the property, we're creating an asynchronous method for appending the fetched data that WeatherService does for us automatically:

func getWeather() async {
        do {
            guard let currentLocation = locationManager.lastLocation else {
                return
            }

            let weather = try await service.weather(for: currentLocation)

            self.currentWeather = Weather(temperature: weather.currentWeather.temperature.value,
                                          condition: weather.currentWeather.condition.rawValue,
                                          symbolName: weather.currentWeather.symbolName,
                                          humidity: weather.currentWeather.humidity,
                                          isDaylight: weather.currentWeather.isDaylight)
        } catch {
            assertionFailure(error.localizedDescription)
        }
    }

The currentWeather class, instanced in WeatherService, provides some other properties, such as date, dewPoint, pressureTrend and more. We're only covering a few of them here. Refer to the documentation for more.

Speaking about WeatherService, it provides other instances, such as daily, hourly, minute and others. The mentioned instances are to fetch daily weather data for the next 10 days, hourly and minute for a specific time. We're only using currentWeather of it. Read the documentation for more.

Our data model is now fully ready. Let's jump to the main view's file, ContentView. We're showing the weather condition with an SF Symbol icon, a temperature label, and an SF Symbol for the humidity with a label for its rate.

struct ContentView: View {

    @ObservedObject var weatherViewModel: WeatherViewModel

    var body: some View {
        ScrollView(.vertical, showsIndicators: false) {


            VStack {
                WeatherView(weather: weatherViewModel.currentWeather,
                            dailyWeather: weatherViewModel.dailyWeather)
            }
            .frame(maxWidth: .infinity)
        }
        .ignoresSafeArea(.all)
        .background(LinearGradient(colors: weatherViewModel.currentWeather.isDaylight
                                   ? [.blue.opacity(0.7), .blue]
                                   : [.black.opacity(0.7), .black],
                                   startPoint: .top, endPoint: .bottom))
        .onAppear {
            Task {
                await weatherViewModel.getWeather()
            }
        }
    }
}

struct WeatherView: View {
    let weather: Weather

    var body: some View {
        VStack {

            Spacer()

            VStack(spacing: 30) {
                Image(systemName: weather.symbolName)
                    .resizable()
                    .scaledToFit()
                    .foregroundColor(.white)
                    .frame(width: 80, height: 80)

                Text(String(format: "%.0f", weather.temperature) + "ยฐC")
                    .foregroundColor(.white)
                    .font(.largeTitle)

                Text(weather.condition.uppercased())
                    .foregroundColor(.white)
                    .font(.body)

                HStack {
                    HStack(spacing: 10) {
                        Image(systemName: "humidity.fill")
                            .resizable()
                            .scaledToFit()
                            .foregroundColor(.white)
                            .frame(width: 20, height: 20)

                        Text(String(format: "%.0f", weather.humidity) + "%")
                            .foregroundColor(.white)
                            .font(.body)
                    }
                }
            }
            .padding(.top, 80)

            Spacer()
        }
        .padding(.top, 50)
    }
}

extension Double {
    func formattedValue(style: NumberFormatter.Style = .decimal) -> String {
        let formatter = NumberFormatter()
        formatter.numberStyle = style

        return formatter.string(from: NSNumber(value: self)) ?? ""
    }
}

Final result: ๐Ÿช„

Simulator Screen Shot - iPhone 13 Pro - 2022-07-23 at 01.54.02.png

Finally, I want thank the Apple's Weather team for building such handy framework, especially Novall Swift since she is the only person I know there!

ย