I have an app that has been using the following code to down load audio files:
if let url = URL(string: episode.fetchPath()) {
var request = URLRequest(url: url)
request.httpMethod = "get"
let task = session.downloadTask(with: request)
And then the following completionHandler code:
func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
try FileManager.default.moveItem(at: location, to: localUrl)
In the spirit of modernization, I'm trying to update this code to use async await:
var request = URLRequest(url: url)
request.httpMethod = "get"
let (data, response) = try await URLSession.shared.data(for: request)
try data.write(to: localUrl, options: [.atomicWrite, .completeFileProtection])
Both these code paths use the same url value. Both return the same Data blobs (they return the same hash value) Unfortunately the second code path (using await) introduces a problem. When the audio is playing and the iPhone goes to sleep, after 15 seconds, the audio stops. This problem does not occur when running the first code (using the didFinish completion handler) Same data, stored in the same URL, but using different URLSession calls. I would like to use async/await and not have to experience the audio ending after just 15 seconds of the device screen being asleep. any guidance greatly appreciated.
Background execution is tricky on iOS. I’ve collected together a bunch of links to useful resources in Background Tasks Resources. Specifically, make sure you read Testing and Debugging Code Running in the Background.
I would like to use async/await and not have to experience the audio ending after just 15 seconds of the device screen being asleep.
You’re wrangling two completely separate axes of background execution here:
- Background audio
URLSession
Background audio is the most straightforward, at least conceptually: If your app supports background audio (UIBackgroundModes contains audio) and you start playing in the foreground, iOS will keep your app executing as it moves between the foreground and background as long as your app continues to play audio.
Note I can only help you with the conceptual side of this. The actual mechanics of audio playback are outside my area of expertise.
URLSession has two background execution models:
- Background sessions
- Standard sessions
In a background session, the system is allowed to suspend (and even terminate) your app when it’s in the background. When all the requests in the background session complete, the system will resume (or relaunch) the app in the background to process the results.
Standard sessions behave like all other networking on iOS: They work just fine as long as your app is running. If your app gets suspended, network connections simply tear.
So, these two axes are connected when you create an audio streaming app. If you’re streaming audio in the background, you can use a standard session for your networking because your audio playback prevents your app from being suspended.
Next, let’s talk about tasks:
- Background sessions are primarily focused on download and upload tasks.
- They do support data tasks but those tasks only work while your app is running. If your app gets suspended, any data tasks in a background session will fail. Thus, running data tasks in a background session is something you’d do only in very odd circumstances.
- Background sessions don’t support the convenience APIs. This includes the completion handler APIs and the Swift async functions which are layered on top of them. You must use the delegate-based APIs.
In general, it’s hard to model a task in a background session as a Swift async function. That’s because of the terminate-and-relaunch behaviour I described above.
While a Swift async function is async under the covers, conceptually it’s synchronous. You call a function and it doesn’t return until it’s complete. Thus, any reasonable program builds up a call stack, where function A calls function B calls function C, and so on. This call stack isn’t stored on the thread stack, like it would be for a Swift synchronous function, but it still exists.
And, critically, it exists in memory. If the process gets terminated, the state of all those async functions goes away. There’s no way rebuild that state when the process is relaunched.
Given that, you can’t model a task in a background session as a Swift async function. Tab A just doesn’t fit into slot B.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"