The application fetches the location of the device on launch and it subsequently asks the Dark Sky API for weather data for that location. That's fine, but it isn't sufficient. What happens if the user backgrounds the application and opens it several hours later. The weather data may be out of date and, if they're traveling, the location of the device may have changed.
The solution is simple. The application should fetch the weather data for the current location of the device every time the user opens the application. That's the focus of this episode.
Observing Notifications
You probably know that the application delegate is notified when the application is about to move from the background to the foreground. The system notifies the application delegate by invoking the applicationWillEnterForeground(_:) method.
AppDelegate.swift
func applicationWillEnterForeground(_ application: UIApplication) {
// Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}
The system also posts a UIApplicationWillEnterForeground notification, allowing other objects to be notified when the application is about to enter the foreground. Observing the notification is more convenient for us because we would like the RootViewModel class to respond to this event, not the application delegate.
Open RootViewModel.swift. In the initializer of the RootViewModel class, we invoke a helper method, setupNotificationHandling(). I always use helper methods to keep the implementation of initializers as short as possible.
// MARK: - Initialization
init(locationService: LocationService) {
// Set Location Service
self.locationService = locationService
super.init()
// Fetch Weather Data
fetchWeatherData(for: Defaults.location)
// Setup Notification Handling
setupNotificationHandling()
// Fetch Location
fetchLocation()
}
In setupNotificationHandling(), we register the RootViewModel instance as an observer for UIApplicationWillEnterForeground notifications by invoking the addObserver(forName:object:queue:using:) method. The method accepts four arguments, the name of the notification, the object whose notifications the RootViewModel instance wants to receive, an operation queue, and the closure that is executed when a notification is received.
private func setupNotificationHandling() {
// Application Will Enter Foreground
NotificationCenter.default.addObserver(forName: Notification.Name.UIApplicationWillEnterForeground, object: nil, queue: OperationQueue.main) { (_) in
}
}
We pass nil as the second argument, which indicates that the RootViewModel instance receives every notification with name UIApplicationWillEnterForeground. This is fine since only the UIApplication singleton posts notifications with that name.
We pass a reference to the operation queue that is tied to the main thread as the third argument. This is less important, but it means that we can be sure that the notifications arrive on the main thread.
How should the application respond when a notification arrives? The idea is simple. The RootViewModel instance should fetch weather data for the current location of the device. This means we need to invoke the fetchLocation() method. To make it clear what is about to happen, I'd like to create and invoke a helper method, refresh().
private func setupNotificationHandling() {
// Application Will Enter Foreground
NotificationCenter.default.addObserver(forName: Notification.Name.UIApplicationWillEnterForeground, object: nil, queue: OperationQueue.main) { (_) in
self.refresh()
}
}
To prevent the notification center from holding a strong reference to the RootViewModel instance, we weakly reference self in the closure we pass to the addObserver(forName:object:queue:using:) method, using a capture list.
private func setupNotificationHandling() {
// Application Will Enter Foreground
NotificationCenter.default.addObserver(forName: Notification.Name.UIApplicationWillEnterForeground, object: nil, queue: OperationQueue.main) { [weak self] (_) in
self?.refresh()
}
}
The implementation of the refresh() method isn't complicated. We invoke the fetchLocation() method. That's it.
private func refresh() {
fetchLocation()
}
Let's test if the current implementation works as expected. Add a breakpoint to the refresh() method. The refresh() method should be invoked every time the application moves from the background to the foreground.
Build and run the application. The application first fetches weather data for the default location and, once it has obtained the location of the device, it fetches weather data for that location. That isn't new.
Push the application to the background and, after a few moments, tap the application icon to bring it back to the foreground. The application should break on the breakpoint we set.
Improving Efficiency
Every time the application moves from the background to the foreground, it obtains the location of the device and fetches weather data for that location. This happens every time, but that isn't always necessary. If the user pushes the application to the background, makes a phone call, and returns to the application, the location of the device won't have changed and neither will the weather conditions for that location.
We need to control how frequently the application refreshes the location and the weather data. This is easy to do. If the application successfully fetches weather data, we store a timestamp in the user defaults database. The next time the application tries to fetch the location and the weather data, we first make sure it makes sense to do so.
We start simple. Open RootViewModel.swift and create an extension for UserDefaults at the bottom. You can put the extension in a separate file. That's a personal choice.
extension UserDefaults {
}
Because I don't like random string literals in a project, we define a private enum in the extension, Keys, that defines a static constant property, didFetchWeatherData, of type String.
extension UserDefaults {
// MARK: - Types
private enum Keys {
static let didFetchWeatherData = "didFetchWeatherData"
}
}
I usually create a class computed property to store small pieces of data in the user defaults database. This is easy to do. We define a class computed property, didFetchWeatherData, of type Date?. Notice that we mark the computed property with the fileprivate keyword. That makes it only accessible from within RootViewModel.swift.
extension UserDefaults {
// MARK: - Types
private enum Keys {
static let didFetchWeatherData = "didFetchWeatherData"
}
// MARK: - Class Computed Properties
fileprivate class var didFetchWeatherData: Date? {
}
}
We define a getter and a setter. The getter returns the object for the key we defined in the Keys enum. We cast the object to a Date instance. In the setter, we set the value stored in newValue for the key we defined in the Keys enum.
extension UserDefaults {
// MARK: - Types
private enum Keys {
static let didFetchWeatherData = "didFetchWeatherData"
}
// MARK: - Class Computed Properties
fileprivate class var didFetchWeatherData: Date? {
get {
return UserDefaults.standard.object(forKey: Keys.didFetchWeatherData) as? Date
}
set(newValue) {
UserDefaults.standard.set(newValue, forKey: Keys.didFetchWeatherData)
}
}
}
The convenience of creating a class computed property becomes clear in a moment. Revisit setupNotificationHandling() in RootViewModel.swift. If no timestamp is stored in the user defaults database, it means the application hasn't fetched weather data for the location of the device. In that scenario, it's fine to invoke the refresh() method.
We use a guard statement to safely access the timestamp stored in the user defaults database. In the else clause of the guard statement, we invoke refresh().
private func setupNotificationHandling() {
// Application Will Enter Foreground
NotificationCenter.default.addObserver(forName: Notification.Name.UIApplicationWillEnterForeground, object: nil, queue: OperationQueue.main) { [weak self] (_) in
guard let didFetchWeatherData = UserDefaults.didFetchWeatherData else {
self?.refresh()
return
}
}
}
To decide whether it's appropriate to refresh the location and the weather data, we need to find out how much time has passed since the application last fetched weather data. We calculate the number of seconds that have passed since the last successful weather data request and make sure that it is larger than 600.0 or ten minutes.
private func setupNotificationHandling() {
// Application Will Enter Foreground
NotificationCenter.default.addObserver(forName: Notification.Name.UIApplicationWillEnterForeground, object: nil, queue: OperationQueue.main) { [weak self] (_) in
guard let didFetchWeatherData = UserDefaults.didFetchWeatherData else {
self?.refresh()
return
}
if Date().timeIntervalSince(didFetchWeatherData) > 10.0 * 60.0 {
self?.refresh()
}
}
}
I prefer to keep constants that configure the behavior of the application in a central location. Open Configuration.swift and define an enum with name Configuration. Define a static, variable property, refreshThreshold, of type TimeInterval.
enum Configuration {
static var refreshThreshold: TimeInterval {
}
}
To make the development of the application easier, we return a different value for debug builds. That's a convenient trick that makes sure we don't need to change the value during development every time we want to test something.
enum Configuration {
static var refreshThreshold: TimeInterval {
#if DEBUG
return 15.0
#else
return 10.0 * 60.0
#endif
}
}
Revisit RootViewModel.swift and put the refreshThreshold property to use.
private func setupNotificationHandling() {
// Application Will Enter Foreground
NotificationCenter.default.addObserver(forName: Notification.Name.UIApplicationWillEnterForeground, object: nil, queue: OperationQueue.main) { [weak self] (_) in
guard let didFetchWeatherData = UserDefaults.didFetchWeatherData else {
self?.refresh()
return
}
if Date().timeIntervalSince(didFetchWeatherData) > Configuration.refreshThreshold {
self?.refresh()
}
}
}
Before we can test the implementation, we need to update the fetchWeatherData(for:) method. Every time the application successfully fetches weather data from the Dark Sky API, we update the timestamp stored in the user defaults database.
private func fetchWeatherData(for location: Location) {
// Initialize Weather Request
let weatherRequest = WeatherRequest(baseUrl: WeatherService.authenticatedBaseUrl, location: location)
// Create Data Task
URLSession.shared.dataTask(with: weatherRequest.url) { [weak self] (data, response, error) in
if let response = response as? HTTPURLResponse {
print("Status Code: \(response.statusCode)")
}
DispatchQueue.main.async {
if let error = error {
...
} else if let data = data {
...
do {
// Decode JSON Response
let darkSkyResponse = try decoder.decode(DarkSkyResponse.self, from: data)
// Weather Data Result
let result: WeatherDataResult = .success(darkSkyResponse)
// Update User Defaults
UserDefaults.didFetchWeatherData = Date()
// Invoke Completion Handler
self?.didFetchWeatherData?(result)
} catch {
...
}
} else {
...
}
}
}.resume()
}
Testing the implementation is easy thanks to the refresh threshold we defined for debug builds. Set a breakpoint in the refresh() method. Build and run the application.
No matter how frequently you bring the application to the foreground, the application won't refresh the location and the weather data until the refresh threshold is hit.
What's Next?
Making sure an application efficiently uses its resources is important. Remember that the Dark Sky API is a paid weather service. By making the application more efficient, you reduce the amount of energy the application consumes and you save money by limiting the number of requests the application sends to the Dark Sky API. This doesn't seem important, but I can assure you that it is essential as your application grows and increases in complexity.