Downloading data from the network follows the same general pattern as background tasks.
But, they’re a bit trickier. Network downloads require delegates, and there could be more than one running at a time.
Like background tasks, you can run up to four downloads per hour.
Unlike background tasks, you can run all four at once if you wish.
While the specific details are unclear, Apple warns that the actual number depends on factors such as Wi-Fi availability, cellular signal strength and battery life.
URLSession setup and configuration
To start, create a file named UrlDownloader.swift
And declare a URLDownload class. It will need to implement URLSessionDownloadDelegate, so it has to subclass from NSObject.
final class UrlDownloader: NSObject {
}
Background URLSession tasks are assigned identifiers, so you need to provide callers with a way to specify which identifier to use. Subclassing NSObject means you’ll need to provide an explicit initializer to deal with that.
let identifier: String
init(identifier: String) {
self.identifier = identifier
}
URLSession should only be created once, on-demand.
private lazy var backgroundUrlSession: URLSession = {
}()
We’ll need to do a bit of configuration.
Background downloads need a special URLSessionConfiguration, initialized with your desired identifier.
let config = URLSessionConfiguration.background(
withIdentifier: identifier
)
If you set isDiscretionary to false, which is the default, you’re telling watchOS that it should try to run your download as soon as you ask it to, instead of letting it determine the best time.
config.isDiscretionary = false
Setting sessionSendsLaunchEvents to true, the default, tells watchOS to automatically wake up or launch your app in the background when required.
config.sessionSendsLaunchEvents = true
Now we can initialize a URLSession with that configuration, set the delegate to self, and set nil for the delegateQueue parameter.
Passing in nil, here, means watchOS will create a serial operation queue to handle the delegate callbacks.
return .init(
configuration: config,
delegate: self,
delegateQueue: nil
)
We’ve said that URLDownloader will act as it’s own delegate, but we haven’t set that up yet.
So, set up an extension to hold our delegate code.
And add an empty implementation of the lone required method.
extension UrlDownloader: URLSessionDownloadDelegate {
func urlSession(
_ session: URLSession,
downloadTask: URLSessionDownloadTask,
didFinishDownloadingTo location: URL
) {
}
}
We’ll come back to this in a bit. We’re just setting it up now so Xcode doesn’t bother us with errors.
Scheduling a network download
Now, similar to how we created a background task in the previous episode, we can create a URL Sessions Download Task.
Add a new property to UrlDownloader to hold that.
private var backgroundTask: URLSessionDownloadTask?
Then implement the scheduling method, starting out just like we did last time. The initial setup, including date calculation, is the same as for background tasks.
func schedule(firstTime: Bool = false) {
let minutes = firstTime ? 1 : 15
let when = Calendar.current.date(
byAdding: .minute,
value: minutes,
to: Date.now
)!
With the URL you plan to download from, and the backgroundSession we just configured, generate a download task.
let url = URL(
string: "https://api.weather.gov/gridpoints/TOP/31,80/forecast"
)!
let task = backgroundUrlSession.downloadTask(with: url)
Now we should set earliestBeginDate. This is how you let watchOS know that it shouldn’t start the network download before a certain date.
task.earliestBeginDate = when
If the API you call uses caching headers, use those to help define the earliest beginning date.
To help watchOS optimize when to perform the download, you can specify exactly how many bytes you expect to send, including header count, and receive.
task.countOfBytesClientExpectsToSend = 100
task.countOfBytesClientExpectsToReceive = 12_000
Pay close attention to the property names! There are multiple properties with almost the same name, and it’s easy to get confused.
To start the download, call resume. If you don’t do this, the network download will never start.
task.resume()
Finally, store the task in your class’ property to use in the delegate methods.
backgroundTask = task
}
Note: Use the delegate pattern for background URL downloads. You may not use the newer
asyncmethods.
URLSessionDownloadDelegate
When the backgroundTask finishes downloading, watchOS will call this urlSession(_:downloadTask:didFinishDownloadingTo:) method we left empty earlier.
The data provided by the API call is JSON, so, we’ll need a decoder.
let decoder = JSONDecoder()
Now we’ve got to perform several checks before we can properly update our local data.
guard
else {
return
}
First, the location you provide should be a file URL. The check is probably not entirely necessary, but better safe than sorry.
guard
location.isFileURL,
else {
return
}
Next, read the contents of the file that watchOS wrote the data to.
let data = try? Data(contentsOf: location),
Then, decode the data based on the Weather structure provided with the sample project.
let decoded = try? decoder.decode(Weather.self, from: data),
We’re just going to grab the first temperature in there. That isn’t really the current temperature, but it’ll work for our example.
let temperature = decoded.properties.periods.first?.temperature
else {
return
}
With all of that guaranteed, store the downloaded temperature to UserDefaults so that you can access it in the complication.
UserDefaults.standard.set(temperature, forKey: "temperature")
The provided ComplicationController class displays the temperature stored in this location.
Keep in mind, when this delegate method ends, watchOS will automatically delete the file at location.
If you download an image or movie, copy the file somewhere appropriate. If you downloaded JSON data, like in this example, store the data as needed in something like Core Data, @AppStorage or UserDefaults.
Once the session has completed, watchOS will call the urlSession(_:task:didCompleteWithError:) delegate method.
So, let’s add that to our delegate:
func urlSession(
_ session: URLSession,
task: URLSessionTask,
didCompleteWithError error: Error?
) {
}
Set the background task to nil. And recall that the delegate methods run on a serial dispatch queue. When you call the completion handler, you need to dispatch that back to the main queue.
backgroundTask = nil
DispatchQueue.main.async {
self.completionHandler?(error == nil)
self.completionHandler = nil
}
}
If your network download requires extra delegate events, such as authentication challenges, you’ll need to call the completion handler from the urlSessionDidFinishEvents(forBackgroundURLSession:) delegate method as well.
But you must not schedule a new download at that point because the download itself hasn’t happened yet.
Xcode isn’t so happy right now, because we don’t actually have a completion handler.
Preparing for download
So let’s add one at the top of UrlDownloader:
private var completionHandler: ((Bool) -> Void)?
Then implement the perform method:
public func perform(_ completionHandler: @escaping (Bool) -> Void) {
self.completionHandler = completionHandler
_ = backgroundUrlSession
}
That looks pointless.
You’ve stumbled upon the major confusion of background URL downloads.
Your app may go in and out of background mode while the download occurs. When watchOS re-attaches your app, you have to let it know that it should reuse the previous session, so it will call the delegate methods properly.
By recreating a session with the exact same identifier that you originally used, watchOS ties everything together for you.
Assigning to the _ variable discards the result. You don’t need to hold on to it, you just need the happy side effect of creating the session again if it doesn’t already exist.
-
ExtensionDelegatenetwork download
Alright, let’s tie this all together in back in ExtensionDelegate.swift
First, add a property to track network downloads:
private var downloads: [String: UrlDownloader] = [:]
Then add a method to manage that dictionary. It will return a UrlDownloader for a given identifier.
private func downloader(for identifier: String) -> UrlDownloader {
}
If the UrlDownloader for that identifier doesn’t already exist, create a new one.
guard let download = downloads[identifier] else {
let downloader = UrlDownloader(identifier: identifier)
downloads[identifier] = downloader
return downloader
}
If it does exist, directly return it.
return download
Now we can add another case to the switch statement, this time for WKURLSessionRefreshBackgroundTask
case let task as WKURLSessionRefreshBackgroundTask:
The code will be quite similar to the WKApplicationRefreshBackgroundTask code:
Using your helper method, grab the appropriate UrlDownloader instance for the task’s sessionIdentifier.
let downloader = downloader(for: task.sessionIdentifier)
Call the perform method, and pass in a completion handler to call when the download completes.
downloader.perform { updateComplications in
}
Like before, update your complications, schedule the next download, and mark the task as complete.
if updateComplications {
Self.updateActiveComplications()
}
downloader.schedule()
task.setTaskCompletedWithSnapshot(false)
}
To let us test this out, we’ll add a bit of UI to this app.
Updating ContentView
In ContentView.swift, add a URLDownloader.
@State private var downloader = UrlDownloader(identifier: "ContentView")
Remember, you can use any name for the identifier parameter. It just has to stay consistent for the same type of downloads.
Then, add a button to the body to schedule the first download.
Button {
downloader.schedule(firstTime: true)
} label: {
Text("Download")
}
You’ll need a live Apple Watch to test this out, so build and run the app to that device. I am just going to show you the steps here in the simulator because it’s easier to record than the actual watch!
Once it launches, return to the home screen and add the Updates complication to your watch face.
Tap the complication to launch the app, and then tap Download.
Press the Digital Crown to go back to the home screen and then drop your wrist.
Around a minute from now, watchOS will perform the background download and update the complication on your watch face, showing a temperature.