Meet WeatherKit AND Swift Charts
This year at WWDC22 there was lots of exciting new frameworks and apis for us to sink our teeth into. However I’m really excited about WeatherKit and Swift Charts because we now finally have native 1st party support for weather data and charting our datasets.
In this article we will look at how we can combine the two to visualise our weather data. Its been something like 30 hours since keynote so bear mind that the code you see will probably change over the next few months. I’ve also ran into some unsupported Chart commands that tosses a fatal error.
Swift Charts
So what is Swift Charts? Its a way to graph your data easily and simply across various chart styles (as of writing this; pie charts, radial graphs, etc aren’t supported) that also supports all the lovely accessibility features, animations, and more.
In the past I’ve had to write my own charts or bring in a 3rd party dependency to achieve this and even then accessibility features and animations weren’t exactly in mind.
WeatherKit
Apple acquired the popular Dark Sky weather app back in March 2020, leaving developers wondering what would replace the retired Dark Sky API and now we have the official answer. Developers can now use a native, privacy-focused solution instead of relying on 3rd party providers.
Even at this early beta stage, the documentation shows the API is very comprehensive, covering a range of weather data, alerts, forecasts and celestial information.
Although regular use of the API has a pricing model attached, members of the Apple Developer program can use up to 500,000 calls a month before being charged.
Dark Sky API will officially sunset on the 23rd of March 2023, but Apple provides migration documentation for developers.
Combining both Swift Charts and WeatherKit
Now let’s take a look at combing these two to represent daily temperatures at a given location.
First let’s go ahead and create a new project make sure to target SwiftUI.
Next to enable WeatherKit we need to go to the Apple Developer Dashboard and go into Identifiers. Once in there let’s register an App ID. Go into the App Services tab and select the WeatherKit checkbox. Click continue, review the information, and click register. Also remember to enable WeatherKit in the App Capabilities tab when editing your App ID.
Now we should be ready to start consuming the WeatherKit data.
Let’s start off creating a WeatherFetcher class that conforms to ObservableObject.
class WeatherFetcher: ObservableObject { … }
Next since we are wanting a list of daily temperatures, we need to represent this.
For us to represent this data appropriately for our users, we need to have the day, the low and high temperatures. Since we want to represent this in a Chart the model needs to conform to Identifiable.
struct DailyTemperature: Identifiable {
let day: Date
let min: Double
let max: Double
let id: String
}
So now that we have this model, we can look at fetching the data from WeatherKit.
Let’s create a function called fetchDaily, since the WeatherKit service is async we need add an async keyword here.
class WeatherFetcher: ObservableObject { func fetchDaily() async { .... }
}
Before we can consume WeatherKit we will need a few imports. So at the top of the file let’s import following
import WeatherKit
import CoreLocation
Now that is out of the way we should be able to start fetching the data. Let’s initialise a WeatherService, this will allow us to fetch weather from it. Afterwards I decided to just hardcode coordinates for this but of course this can be dynamic as well. After having the coordinates we will pass this to the WeatherService. This weather function has an additional parameter that is a variadic generic for WeatherQuery which let’s you add multiple parameters to the query such as daily, current, alerts, etc.
func fetchDaily() async {
let weatherService = WeatherService()
let melbourne = CLLocation(
latitude: -37.815018,
longitude: 144.946014
) let weather = try! await weatherService.weather(for: melbourne)
}
If everything is good we should now have data in the weather property. I didn’t setup the permissions properly for the AppID and ended up seeing a 401 printed to console.
Now that we have the weather data lets map this into something we can consume.
At the top, lets create a Published property for an array of daily temperatures. Building on from the previous snippet lets take the weather variable and grab forecast from daily forecast, this will give us an array of DayWeather. For this example I decided to just only care about the first 5 entries.
So after grabbing the first 5 DayWeather elements let’s map this data into the model we created early, DailyTemperature. Also since we don’t get really any unique IDs or well anything unique for that matter I’m passing in UUID string for the id.
Now that we have the data the way we like it, lets set this on the Published variable. Make sure to set this on the main thread.
class WeatherFetcher: ObservableObject {
@Published var dailyTemperatures: [DailyTemperature] = [] func fetchDaily() async {
.......
.......
.......
let dailyForecasts = weather.dailyForecast.forecast let dailyTemperatures = Array(
dailyForecasts.prefix(5)
).map {
DailyTemperature(
day: $0.date,
min: $0.lowTemperature.value,
max: $0.highTemperature.value,
id: UUID().uuidString
)
}
DispatchQueue.main.async {
self.dailyTemperatures = dailyTemperatures
}
}
}
SwiftUI and Swift Charts
We will now build the SwiftUI part and consume the data that we just fetched prior. By the end of this section what we are building should look roughly like this.
So let’s get a basic setup going for your SwiftUI view.
By now you should have the top 3 imports already (depending if you’re using a separate file or not). Let’s go ahead and add an import for Charts.
Down in the ContentView lets add an ObservedObject property for WeatherFetcher that we just created so we consume the data we want to fetch.
For this I’m just hardcoding the city into the Text.
import SwiftUI
import WeatherKit
import CoreLocation
import Chartsstruct ContentView: View {
@ObservedObject var fetcher = WeatherFetcher() var body: some View {
VStack {
Text("Melbourne daily temps")
.font(.title)
.frame(maxWidth: .infinity, alignment: .leading)
}
}
}
Building on top of the previous snippet let’s add a task to the VStack. This will be for our fetchDaily function call due to it being an async/await call.
struct ContentView: View {
@ObservedObject var fetcher = WeatherFetcher() var body: some View {
VStack {
.....
}
.task {
await fetcher.fetchDaily()
}
}
}
Let’s now add Chart into our VStack just below the Text. The api acts like a List as it takes in an array of Identifiables and loops on it. We will pass in dailyTemperatures from the fetcher.
struct ContentView: View {
@ObservedObject var fetcher = WeatherFetcher() var body: some View {
VStack {
.....
Chart(fetcher.dailyTemperatures) { daily in
.....
}
}
......
}
}
In its current state it won’t run so we need to add actual UI to the Chart. We will be adding a RuleMark which is just a multiple lines that can exist on a vertical or horizontal chart. For this we will focus on vertical straight lines representing a min and max. With the yStart and yEnd it doesn’t matter here which way I put the min and max values.
struct ContentView: View {
@ObservedObject var fetcher = WeatherFetcher() var body: some View {
VStack {
.....
Chart(fetcher.dailyTemperatures) { daily in
RuleMark(
x: .value("Day", daily.day),
yStart: .value("Low temperature", daily.low),
yEnd: .value("High temperature", daily.high)
)
.foregroundStyle(.black)
}
}
......
}
}
By this point you should see something like below.
To give this a bit more visuals let’s add points at both the min and max. Let’s add PointMark to the chart. These are just represented by x and y values. With the Date that I’m passing in as a day, the value has an additional parameter for units which will format your Date.
I’m adding foregroundStyle(by:) here so I can group these points and let the system colour them apporiately.
struct ContentView: View {
@ObservedObject var fetcher = WeatherFetcher()var body: some View {
VStack {
.....
Chart(fetcher.dailyTemperatures) { daily in
.....
PointMark(
x: .value("Day", daily.day),
y: .value("Low temperature", daily.min)
)
.foregroundStyle(by: .value("Low", daily.min)) PointMark(
x: .value("Day", daily.day),
y: .value("High temperature", daily.max)
)
.foregroundStyle(by: .value("High", daily.max))
}
}
......
}
}
Now we should have a Chart that looks somewhat like below. There could be more visual improvements here however after trial and error I still wasn’t able to style the axises the way I wanted.
Edit: 8 hours or so after this was posted, Raise The Bar session video was posted which covered styling a bit more. I’ve made updates to the Github repository to update the chart styling.
This just touches the surface of what is available in both Charts and WeatherKit. As you can see its very simple but also very powerful in what you can do.
Repo from the article
https://github.com/collisionspace/WeatherKitCharts