Table of contents
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.
You can go back and select the Services tab if you want to manage your WeatherKit
usage.
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
.
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: ๐ช
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!