WWDC Quick Look 💓 By SwiftGGTeam
Use async/await with URLSession

Use async/await with URLSession

Watch original video

Highlight

iOS 15 adds new URLSessiondata(for:)upload(for:from:)download(from:)bytes(from:)Other native async methods support handling authentication challenges through task-specific delegates, while introducing AsyncSequence for incrementally receiving response bodies.

Core Content

Why introduce the async/await version of URLSession?

A common network request code has three problems using completion handler:

  1. Control flow jumps around: Create task → resume → jump to completionHandler → jump to completionHandler of main queue
  2. Thread safety trap: Three execution contexts are involved (caller queue, session delegate queue, main queue), and the compiler cannot help detect data races.
  3. Difficulty in error handling: completionHandler may be called twice,UIImage(data:)Return optional but no corresponding error

After changing to async/await, the control flow is executed linearly from top to bottom. All codes run in the same concurrency context. There is no need to worry about threading issues and incorrect use.throwProcessing, the compiler will force processing of optional.

async method of URLSession

New async methods in iOS 15 and macOS Monterey include:

  • data(from:) / data(for:): Get data, equivalent to data task convenience method
  • upload(for:from:): Upload data or files, equivalent to upload task
  • download(from:) / download(for:): Download files to disk, not automatically deleted, need to be moved or deleted manually
  • bytes(from:) / bytes(for:):returnAsyncBytes, used to receive response body incrementally

All methods support native Swifttry-catchError handling and Task cancellation.

Handling incremental responses: URLSession.bytes

datauploaddownloadThe methods all wait for the complete response body to arrive before returning.If incremental processing is required (e.g. SSE, streaming JSON), usebytesmethod.

bytes(from:)Returns immediately after the response headers arrive, and the response body ends withAsyncSequence<UInt8>Form incremental delivery.AsyncBytessupplylinesTransformation that parses the response body line by line.

Task-specific Delegate

The delegate model of URLSession provides callbacks such as authentication challenges and metrics.The new async method no longer exposes the underlying task. How to handle authentication challenges for specific tasks?

The answer is to pass a task-specific delegate parameter to the async method.This delegate only handles events for that specific task and is strongly referenced by the task until completion or failure.

If both session delegate and task delegate implement the same method, the implementation of task delegate takes precedence.Note: Background URL Session does not support task-specific delegates.

Swift Concurrency cancellation mechanism

TaskThe types arecancel()method, but be aware of Swift Concurrency’sTaskandURLSessionTaskThey are completely different concepts, just with similar names.

Cancellation is collaborative - callcancel()After that, the code is at the next suspend point (e.g.await) checks the cancellation status and throwsCancellationError

Detailed Content

Get the async version of the photo

02:52

func fetchPhoto(url: URL) async throws -> UIImage
{
    let (data, response) = try await URLSession.shared.data(from: url)

    guard let httpResponse = response as? HTTPURLResponse,
          httpResponse.statusCode == 200 else {
        throw WoofError.invalidServerResponse
    }

    guard let image = UIImage(data: data) else {
        throw WoofError.unsupportedImage
    }

    return image
}

Key points:

  • data(from:)return(Data, URLResponse)tuple
  • useguardCheck the status code, if it does not match thethrowmistake
  • UIImage(data:)Return optional, must be processednilCondition
  • The whole function isasync throws, use when callingtry await

URL Request version of data method

03:45

let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200 else {
    throw MyNetworkingError.invalidServerResponse
}

Key points:

  • data(for:)acceptURLRequestParameters, you can configure HTTP headers, methods, etc.
  • The response check mode is the same: guard cast + check status code

Upload files

04:03

var request = URLRequest(url: url)
request.httpMethod = "POST"

let (data, response) = try await URLSession.shared.upload(for: request, fromFile: fileURL)
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 201 else {
    throw MyNetworkingError.invalidServerResponse
}

Key points:

  • upload(for:fromFile:)Upload from disk file, suitable for large files
  • A successful upload usually returns 201 Created, check the corresponding status code
  • Alsoupload(for:from:)version, fromDataupload

Download file

04:21

let (location, response) = try await URLSession.shared.download(from: url)
guard let httpResponse = response as? HTTPURLResponse,
      httpResponse.statusCode == 200 else {
    throw MyNetworkingError.invalidServerResponse
}

try FileManager.default.moveItem(at: location, to: newLocation)

Key points:

  • download(from:)Returns a temporary file URL that needs to be manually moved to the target location
  • The old version of the convenience method will automatically delete the file, but the new version will not
  • Be sure to delete or move temporary files after processing is complete

Task canceled

04:44

let task = Task {
    let (data1, response1) = try await URLSession.shared.data(from: url1)
    let (data2, response2) = try await URLSession.shared.data(from: url2)
}

task.cancel()

Key points:

  • Task { }Create structured concurrency task, returnTask.Handle
  • cancel()in the nextawaitClick to take effect, throwCancellationError
  • if the firstawaithas been returned, cancellation will not affect the second request, and both serial requests will complete

Incrementally receive response body

07:53

let (bytes, response) = try await URLSession.shared.bytes(from: Self.eventStreamURL)
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
    throw WoofError.invalidServerResponse
}
for try await line in bytes.lines {
    let photoMetadata = try JSONDecoder().decode(PhotoMetadata.self, from: Data(line.utf8))
    await updateFavoriteCount(with: photoMetadata)
}

Key points:

  • bytes(from:)return(URLSession.AsyncBytes, URLResponse)tuple
  • AsyncBytes.linesaccording to\nSplit the response body and returnAsyncSequence<String>
  • for try awaitProcess each row in a loop, suitable for SSE and streaming JSON scenarios
  • await updateFavoriteCountCall another async function to update the UI

Task-specific Delegate handles authentication

11:20

class AuthenticationDelegate: NSObject, URLSessionTaskDelegate {
    private let signInController: SignInController

    init(signInController: SignInController) {
        self.signInController = signInController
    }

    func urlSession(
        _ session: URLSession,
        task: URLSessionTask,
        didReceive challenge: URLAuthenticationChallenge
    ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
        if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodHTTPBasic {
            do {
                let (username, password) = try await signInController.promptForCredential()
                return (.useCredential,
                        URLCredential(user: username, password: password, persistence: .forSession))
            } catch {
                return (.cancelAuthenticationChallenge, nil)
            }
        } else {
            return (.performDefaultHandling, nil)
        }
    }
}

How to use:

let delegate = AuthenticationDelegate(signInController: signInController)
let (data, response) = try await URLSession.shared.data(for: request, delegate: delegate)

Key points:

  • The delegate method itself can beasync, supports await calls
  • The delegate is strongly referenced by the task and does not need to be held manually
  • For HTTP Basic authentication, a login box can pop up to obtain credentials
  • return(useCredential, credential)Tell the session to continue the request using the credentials

Core Takeaways

  1. Migrate existing network layer to async/await
  • What to do: Change all completion handler-based network request methods to async throws versions
  • Why it’s worth doing: code readability is greatly improved, error handling is safer, and thread safety bugs are reduced
  • How ​​to start: Start with the most commonly used request method and putcompletionHandler: @escaping (Result<T, Error>) -> VoidChange toasync throws -> T.The entry API isURLSession.data(for:)
  1. Access to bytes API for real-time update interface
  • What to do: UseURLSession.bytes(from:)Alternative polling or WebSocket handling for SSE/streaming interfaces
  • Why is it worth doing: Incrementally receive the response body, lower memory usage and smaller delay
  • How ​​to get started: Check existing SSE interfaces withbytes.linesParse by line, infor try awaitProcess each message in a loop
  1. Use task-specific delegate to simplify authentication logic
  • What to do: Create a dedicated delegate class for requests that require special authentication instead of dispatching with task ID in the global delegate
  • Why is it worth doing: Putting delegate logic and request logic together makes it easier to maintain.
  • How ​​to start: ImplementationURLSessionTaskDelegate,existdata(for:delegate:)Pass in the instance.The delegate method can be marked asasync
  1. Optimize the memory usage of file download
  • What: Use for large file downloadsdownload(from:)instead ofdata(from:)
  • Why it’s worth doing:dataLoad the entire response body into memory,downloadWrite to temporary file, memory usage is constant
  • How ​​to start: Change all pictures and video downloads todownload, use after downloading is completeFileManager.moveItemMove to target location

Comments

GitHub Issues · utterances