WWDC21 - Technical Notes

WWDC21 - Technical Notes

Like the post of last year on WWDC20, I write notes on the videos I watch about WWDC21.

Keynote

  • No technical notes here

Platforms State of the Union

Xcode Cloud

  • 2:47 - CI/CD service built into Xcode and hosted in the cloud. Designed to support all Apple platforms. Extensible with REST APIs. You create and manage Xcode Cloud workflows inside Xcode 13, while test suits, code signing and TestFlight distribution are handled for you. When Xcode Cloud finishes a build, the results are inside Xcode.
  • 6:50 - You can choose for each workflow the versions of Xcode and MacOS used for the compilation. It's also possible to use the Beta versions.
  • 7:50 - Workflow management and Build reports are also available in AppStore Connect on the web.
  • 8:15 - With Xcode Cloud it's possible to configure the workflows to run multiple test plans across multiple platforms, device simulators and OS versions all in parallel (including beta versions).
  • 11.40 - Xcode 13 bring discussions with the team directly inside the editor. It requires the users to be logged in.
  • 14.10 - Code Signing in the Cloud doesn't required to have certificates and profiles up to date on the development machine. You can use now the Archive action to sign for distribution. Now TestFlight is available also for Mac.
  • 14.45 - In iOS 13 there are better tools to connect you to the same diagnostics and feedback found in App Store Connect. Crash Logs from TestFlight apps are delivered to the Organizer within minutes.

Swift Concurrency

  • 21:30 - Now it's possible to make an asynchronous function with async and when you call it, you use await. For example:
    func prepareForShow() async throws -> Scene {
       let dancers = try await danceCompany.warmUp(duration: .minutes(45))
       let scenery = await crew.fetchStageScenery()
       let openingScene = setStage(with: scenery)
       return try await dancers.moveToPosition(in: openingScene)
    }
    
    and you can use the usual Swift constructs, for example:
    func prepareForShow(isStudioRehearsal: Bool) async throws -> Scene {
       let dancers = try await danceCompany.warmUp(duration: .minutes(45))
       let scenery = isStudioRehearsal ? await crew.fetchPracticeScenery() : await crew.fetchStageScenery()
       let openingScene = setStage(with: scenery)
       return try await dancers.moveToPosition(in: openingScene)
    }
    
  • 23:05 - Structure Concurrency is a way to organize concurrent tasks. In the previous example, you might want to execute the first two tasks in parallel:
    func prepareForShow() async throws -> Scene {
       async let dancers = danceCompany.warmUp(duration: .minutes(45))
       async let scenery = crew.fetchStageScenery()
       let openingScene = setStage(with: await scenery)
       return try await dancers.moveToPosition(in: openingScene)
    }
    
    async let variables create child tasks that execute concurrently with the parent. When we need the results of those child tasks, we await the results.
  • 24:36 - Actors are a model for safe concurrent programming and a synchronization primitive. An actor is an object that protects its state providing mutually exclusive access.
    actor StageManager {
       var stage: Stage
       
       func setStage(with scenery: Scenery) -> Scene {
          stage.backdrop = scenery.backdrop
          for prop in scenery.props {
             stage.addProp(prop)
          }
          return stage.currentScene
       }
    }
    
    An actor can access its own properties directly, but interacting with an actor externally uses async/await to guarantee mutual exclusion:
    let scene = await stageManager.setStage(with: scenery)
    
  • 26:10 - Actors solve the problem of using the main thread for UI operations. Now it's possible to declare that an operation must be run on the main thread using @MainActor:
    @MainActor
    func display(scene: Scene)
    
    await display(scene: scene)
    

SwiftUI

  • 30:20 - List supports now:
    • swipe to refresh with .refreshable,
    • search field with .searchable,
    • accessibility support with .accessibilityRotor.
  • 31:50 - Multi-table support for Mac with Table and TableColumn.
  • 33:00 - Possibility to add background images to controls, that automatically fades it and adjusts colors to keep the content above it readable.
  • 33:50 - App development with SwiftUI to iPad in Swift Playgrounds 4.

Augmented Reality

  • 40:25 - RealityKit 2 is an update that gives more visual, audio and animation control.
  • 40:45 - With Object Capture it's possible now to create 3D models with iPhone pictures. It generates USDZ files, but also USD and OBJ asset bundles.
  • 45:40 - With the M1 chip, Apple has created a unified Apple graphics platform with a common architecture based on Metal, the Apple GPU and unified memory.

Notifications

  • 51:55 - With the new Interruption Level API, notifications can now provide different levels of urgency. Notification can be assigned to four different levels:
    • passive interruptions: are silent and don't wake up the device;
    • active interruptions: will play a sound or haptic, like notifications today;
    • time sensitive interruptions: are designed to visually stand out and hang on the lock screen a little longer; they will also be announced by Siri, if the user is wearing AirPods;
    • critical alerts: are the most urgent category, they will play a sound even if the device is muted; they required an approved entitlement;
    • communications: they are displayed in a different way, with an avatar the app icon superimposed.
  • 55:30 - Do Not Disturb silences all notifications; with Focus, users can choose apps and people who can send them notifications.

Widgets

  • 1:03:25 - Widgets are now possible on the iPad home screen, and now it's possible to have new extra-large widgets.

SharePlay

  • 1:06:40 - SharePlay allows to define activities with the new Group Activity framework, that can be shared through FaceTime and iMessage.
  • 1:10:00 - it's also possible to share not only audio and video, but also data.

What‘s new in Swift

  • 3:05 - Swift Package Index - it's a page developed by the community to help find packages built with Swift Package Manager
  • 3:30 - This year Apple introduces Swift Package Collections, a curated list of Swift Packages that you can use from command line or Xcode 13.
    With Swift Package Collections you don't need to search for packages in Internet, or copy & paste packages URLs to add them, but simply search and find them in a new Package Search screen in Xcode.
    Package Collection are JSON files that you can publish anywhere.
  • 5:10 - There is an Apple Package Collection containing all open source packages from Apple available in GitHub. Xcode already uses it by default.
  • 5:55 - Swift Collections contains new data structures: Deque, OrderedSet and OrderedDictionary.
    • Deque is like an array, with efficient insertion and removal at both ends;
    • OrderedSet is an hybrid between an array and a set.
    • OrderedDictionary is an alternative to Dictionary where order is important and we need random access to elements.
  • 6:55 - Swift Algorithms contains a collection of sequence and collection algorithms.
  • 7:50 - Swift System is a library that offers access to low level APIs, avaiable on Apple platforms, Linux and Windows. For example API for file paths manipulations.
  • 8:50 - Swift Numerics brings support for Float16 and Complex numbers.
  • 9:30 - Swift Argument Parser is used for argument parsing and is used in Xcode 12.5.
  • 10:05 - Swift on server development: increased performance and functionalities on Linux and AWS Lambda and introduced support for async/await instead of closures.
  • 11:30 - Swift DocC is a documentation compiler built inside Xcode 13. It leverages markdown comments inside the Swift code.
  • 12.50 - Build improvements:
    • faster builds when changing imported modules;
    • faster startup time before launching compiles;
    • fewer recompilations after changing an extension body.
  • 14:25 - Memory Management just works thanks to ARC (Automatic Reference Counting).
    In this year, Swift optimizes how it retains references to objects. It's in the build setting "Optimize Object Lifetimes".
  • 16:30 - Result Builders were originally designed for SwiftUI; it's a flexible way to define complex object hierarchies. This year it has been standardized and refined.
  • 17:00 - Enum Codable synthesis to make an enum conform to Codable automatically; all the necessary code will be written by the compiler.
  • 17:25 - Flexible static member lookup, to have it available for structs like what is already available for enums.
  • 18:25 - Property wrappers on function parameters like what already available today for normal properties.
  • 24:00 - Sample of new network calls:
    let (data, response) = try await URLSession.shared.data(for: request)
    
    Example of fetchImage function:
    func fetchImage(id: String) async throws -> UIImage {
       let request = self.imageURLRequest(for: id)
       let (data, response) = try await URLSession.data(for: request)
       if let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode != 200 {
          throw TransferFailure()
       }
       guard let image = UIImage(data: data) else {
          throw ImageDecodingFailure()
       }
       return image
    }
    
  • 26:30 - Structured Concurrency: to allow operations to run in parallel, and then merge the final results:
    func titleImage() async throws -> Image {
       async let background = renderBackground()
       async let foreground = renderForeground()
       let title = try renderTitle()
       return try await merge(background, foreground, title)
    }
    
    Note: this function will not return if the two background tasks are still running. The background tasks can't outlive the enclosing function.
  • 20:00 - Actors are used to protect data in concurrency scenarios.
    In the example below, the code doesn't work well in multi-thread, where multiple threads can call the increment function at the same time:
    class Statistics {
       private var counter: Int = 0
       func increment() {
          counter += 1
       }
    }
    
    Simply changing this class into a Swift actor, protects it:
    actor Statistics {
       private var counter: Int = 0
       func increment() {
          counter += 1
       }
    }
    
    Actor suspends any operation that can cause data corruptions, so its methods must be called with await when used outside of the actor:
    actor Statistics {
       private var counter: Int = 0
       func increment() {
          counter += 1
       }
       func publish() async {
          await sendResults(counter)
       }
    }
    

Meet async/await in Swift

  • 6:30 - Major issues with async programming with completion handlers are for example forgetting to call the completion handler in case of errors, so the caller is not notified in case of errors and will show a spinner forever.
    Also it's not possible to use the usual exception handling mechanism, that guarantees that the function returns either a value or an error. Swift can't check it, and doesn't give compilation errors.
  • 8:30 - Example of function written with async/await:
    func fetchThumbnail(for id: String) async throws -> UIImage {
       let request = thumbnailURLRequest(for: id)
       let (data, response) = try await URLSession.shared.data(for: request)
       guard (response as? HTTPURLResponse)?.statusCode == 200 else { throw FetchError.badID }
       let maybeImage = UIImage(data: data)
       guard let thumbnail = await maybeImage?.thumbnail else { throw FetchError.badImage }
       return thumbnail
    }
    
  • 13:15 - Async properties are allowed, but only for read-only properties:
    extension UIImage {
       var thumbnail: UIImage? {
          get async {
             let size = CGSize(width: 40, height: 40)
             return await self.byPreparingThumbnail(ofSize: size)
          }
       }
    }
    
    In case needed, in Swift 5.5 properties can also throw.
  • 14:15 - Async sequences:
    for await id in staticImageIDsURL.lines {
       let thumbnail = await fetchThumbnail(for: id)
       collage.add(thumbnail)
    }
    let result = collage.draw()
    
  • 20:05 - Async/await facts:
    • async enables a function to suspend
    • await marks where an async function may suspend execution
    • Other work can happen during a suspension
    • Once an awaited async function completes, execution resumes after the await
  • 21:20 - When testing async code, before you needed to write tests in this way:
    class MockViewModelSpec: XCTestCase {
       func testFetchThumbnail() throws {
          let expectation = XCTestExpectation(description: "mock thumbnails completion")
          self.mockViewModel.fetchThumbnail(for: mockID) { result, error in
             XCAssertNil(error)
             expectation.fulfill()
          }
          wait(for: [expectation], timeout: 5.0)
       }
    }
    
    Now you can simplify in this way:
    class MockViewModelSpec: XCTestCase {
       func testFetchThumbnail() async throws {
          XCTAssertNoThrow(try await self.mockViewModel.fetchThumbnail(for: mockID))
       }
    }
    
  • 22:15 - In a similar way, bridging from sync to async changes from:
    struct ThumbnailView: View {
       @ObservedObject var viewModel: ViewModel
       var post: Post
       @State private var image: UIImage?
       
       var body: some View {
          Image(uiImage: self.image ?? placeholder)
             .onAppear {
                self.viewModel.fetchThumbnail(for: post.id) { result, _ in
                   self.image = result
                }
             }
       }
    }
    
    to:
    struct ThumbnailView: View {
       @ObservedObject var viewModel: ViewModel
       var post: Post
       @State private var image: UIImage?
       
       var body: some View {
          Image(uiImage: self.image ?? placeholder)
             .onAppear {
                async {
                   self.image = try? await self.viewModel.fetchThumbnail(for: post.id)
                }
             }
       }
    }
    
    Here placing the async inside the onAppear, bridges between the async and sync worlds.
  • 25:00 - Swift 5.5 introduces many async APIs in the SDK, for example:
    URLSession.data(with: URLRequest) async throws -> (Data, URLResponse)
    MKDirections.calculateETA() async throws -> ETAResponse
    HKHealthStore.save(_: HKObject) async throws
    NSDocument.share(with: NSSharingService) async
    
  • 27:00 - Async alternatives and continuations - here is an example about how to offer an async alternative to an existing function accepting completion handlers:
    func getPersistentPosts(completion: @escaping ([Post], Error?) -> Void) {
       do {
          let req = Post.fetchRequest()
          req.sortDescriptors = [ NSSortDescriptor(key: "date", ascending: true) ]
          let asyncRequest = NSAsynchronousFetchRequest<Post>(fetchRequest: req) { result in
             completion(result.finalResult ?? [], nil)
          }
          try self.managedObjectContext.execute(asyncRequest)
       } catch {
          completion([], error)
       }
    }
    
    We can refactor it as:
    func getPersistentPosts() async throws -> [Post] {
       typealias PostContinuation = CheckedContinuation<[Post], Error>
       return try await withCheckedThrowingContinuation { (continuation: PostContinuation) in
          self.getPersistentPosts { posts, error in
             if let error = error {
                continuation.resume(throwing: error)
             } else {
                continuation.resume(returning: posts)
             }
          }
       }
    }
    
  • 31:00 - Checked continuations:
    • continuations must be resumed exactly once on every path
    • discarding the continuation without resuming is not allowed
    • Swift will check your work!
  • 32:00 - In case we have event-driven continuations, we can save the continuation and resume it later:
    class ViewController: UIViewController {
       private var activeContinuation: CheckedContinuation<[Post], Error>?
       func sharedPostsFromPeer() async throws -> [Post] {
          try await withCheckedThrowContinuation { continuation in
             self.activeContinuation = continuation
             self.peerManager.syncSharedPosts()
          }
       }
    }
    
    extension ViewController: PeerSyncDelegate {
       func peerManager(_ manager: PeerManager, received posts: [Post]) {
          self.activeContinuation?.resume(returning: posts)
          self.activeContinuation = nil
       }
       func peerManager(_ manager: PeerManager, hadError error: Error) {
          self.activeContinuation?.resume(throwing: error)
          self.activeContinuation = nil
       }
    }
    

Explore structured concurrency in Swift

  • 4:00 - Tasks are a new feature in Swift that provide an async context for executing code concurrently. Swift checks your usage of tasks to help prevent concurrency bugs. Note that when calling an async function a task is not created.
  • 4:50 - async let tasks allows concurrent bindings:
    async let result = URLSession.shared.data(...)
    ...
    try await result
    
    Example of parallel execution:
    func fetchOneThumbnail(withID id: String) async throws -> UIImage {
       let imageReq = imageRequest(for: id), metadataReq = metadataRequest(for: id)
       async let (data, _) = URLSession.shared.data(for: imageReq)
       async let (metadata, _) = URLSession.shared.data(for: metadataReq)
       guard
          let size = parseSize(from: try await metadata),
          let image = try await UIImage(data: data)?.byPreparingThumbnail(ofSize: size)
       else {
          throw ThumbnailFailedError()
       }
       return image
    }
    
    Note: the variables are the same type as in a sequential binding.
    Note: the parent task can finish his task only when all children tasks have completed.
  • 11:00 - Cancellation of tasks is cooperative:
    • Tasks are not stopped immediately when canceled
    • Cancellation can be checked from anywhere
    • Design your code with cancellation in mind
      For example:
    func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
       var thumbnails: [String: UIImage] = [:]
       for id in ids {
          try Task.checkCancellation()
          thumbnails[id] = try await fetchOneThumbnail(withID: id)
       }
       return thumbnails
    }
    
    Two ways to check for cancellation, with and without exception:
    try Task.checkCancellation()
    
    if Task.isCancelled { break }
    }
    
  • 12:55 - Group tasks are used when providing a dynamic amount of concurrency:
    func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
       var thumbnails: [String: UIImage] = [:]
       try await withThrowingTaskGroup(of: Void.self) { group in
          for id in ids {
             group.async {
                thumbnails[id] = try await fetchOneThumbnail(withID: id)
             }
          }
       }
       return thumbnails
    }
    
    But note that the array can't handle more than one update per time.
  • 15:55 - Data-race safety:
    • Task creation taks a @Sendable closure
    • Cannot capture multiple variables
    • Should only capture value types, actors, or classes that implement their own synchronization
      In the example above, for example, you can return a vector of tuples, containing the id and the image:
    func fetchThumbnails(for ids: [String]) async throws -> [String: UIImage] {
       var thumbnails: [String: UIImage] = [:]
       try await withThrowingTaskGroup(of: Void.self) { group in
          for id in ids {
             group.async {
                return (id, try await fetchOneThumbnail(withID: id))
             }
          }
          for try await (id, thumbnail) in group {
             thumbnails[id] = thumbnail
          }
       }
       return thumbnails
    }
    
    With the new for await loop we process the results from the children tasks, and as this loops runs sequentially, we can add each key-value to the final dictionary.
    Note: in case exception inside a group task, all the other children tasks are canceled and then awaited, like for async let. In case of the group task goes out of scope with a normal exit from the block, then cancellation is not implicit.
  • 19:00 - Unstructured tasks give more flexibility at the expense of more manual management. Not all tasks fit a structured pattern:
    • Some tasks need to launch from non-async contexts
    • Some tasks live beyond the confines of a single scope
      For example below, we can't simply use await in a not async context, so we need to embed it in an async closure:
    @MainActor
    class MyDelegate: UICollectionViewDelegate {
       func collectionView(_ view: UICollectionView,
                           willDisplay cell: UICollectionViewCell,
                           forItemAt item: IndexPath) {
          let ids = getThumbnailIDs(for: item)
          async {
             let thumbnails = await fetchThumbnails(for: ids)
             display(thumbnails, in: cell)
          }
       }
    }
    
    With these types of unstructured tasks:
    • Inherit action isolation and priority of the origin context
    • Lifetime is not confined to any scope
    • Can be launched anywhere, even non-async functions
    • Must be manually canceled or awaited
      In case we want more manual control:
    @MainActor
    class MyDelegate: UICollectionViewDelegate {
       var thumbnailTasks: [IndexPath: Task.Handle<Void, Never>] = [:]
       
       func collectionView(_ view: UICollectionView,
                           willDisplay cell: UICollectionViewCell,
                           forItemAt item: IndexPath) {
          let ids = getThumbnailIDs(for: item)
          thumbnailsTasks[item] = async {
             defer { thumbnailTasks[item] = nil }
             let thumbnails = await fetchThumbnails(for: ids)
             display(thumbnails, in: cell)
          }
       }
       
       func collectionView(_ view: UICollectionView,
                           didEndDisplay cell: UICollectionViewCell,
                           forItemAt item: IndexPath) {
          thumbnailsTasks[item]?.cancel()
       }
    }
    
  • 23:20 - Detached tasks are independent from the context:
    • Unscoped lifetime, manually canceled and awaited
    • Do not inherit anything from their originating context
    • Optional parameters control priority and other traits
      For example, after retrieving the thumbnails, we want to write them to a local cache, but not execute this in the main thread:
    @MainActor
    class MyDelegate: UICollectionViewDelegate {
       var thumbnailTasks: [IndexPath: Task.Handle<Void, Never>] = [:]
       
       func collectionView(_ view: UICollectionView,
                           willDisplay cell: UICollectionViewCell,
                           forItemAt item: IndexPath) {
          let ids = getThumbnailIDs(for: item)
          thumbnailsTasks[item] = async {
             defer { thumbnailTasks[item] = nil }
             let thumbnails = await fetchThumbnails(for: ids)
             asyncDetached(priority: .background) {
                writeToLocalCache(thumbnails)
             }
             display(thumbnails, in: cell)
          }
       }
    }
    
    In the detached task I can use all the usual other constructs, for example a task group:
    ...
             let thumbnails = await fetchThumbnails(for: ids)
             asyncDetached(priority: .background)
                withTaskGroup(of: Void.self) { g in
                   g.async { writeToLocalCache(thumbnails) }
                   g.async { log(thumbnails) }
                   g.async { ... }
                }
    ...
    
    For example canceling the detached task will cancel all the tasks in the task group. Also children tasks inherit the priority of their parent.
  • 25:50 - Here is a slide that summarize all the types of available tasks:
    Flavors-of-tasks

Protect mutable state with Swift actors

  • 0:30 - Data races happen when two thread concurrently access the same data and one of them is a write.
    class Counter {
       var value = 0
       
       func increment() -> Int {
          value = value + 1
          return value
       }
    }
    
    let counter = Counter()
    asyncDetached {
       print(counter.increment())
    }
    asyncDetached {
       print(counter.increment())
    }
    
  • 2:25 - Value semantics help eliminate data races. The majority of Swift types, including dictionaries and array, have value semantic. For example we can convert the previous example in a struct:
    struct Counter {
       var value = 0
       
       mutating func increment() -> Int {
          value = value + 1
          return value
       }
    }
    
    let counter = Counter()
    asyncDetached {
       var counter = counter
       print(counter.increment())
    }
    asyncDetached {
       var counter = counter
       print(counter.increment())
    }
    
    Now we don't have data races anymore, but this is not sharing the state, the behavior is not what we want.
  • 4:40 - Actors provide synchronization for shared mutable state. They isolate their state from the rest of the program, and the only way to go to their state is through the actor, that ensures mutually-exclusive access to its state.
  • 5:25 - Actors have capabilities similar to structs, enum, and classes. They are reference types.
    actor Counter {
       var value = 0
       
       func increment() -> Int {
          value = value + 1
          return value
       }
    }
    
    let counter = Counter()
    asyncDetached {
       print(await counter.increment())
    }
    asyncDetached {
       print(await counter.increment())
    }
    
  • 7:25 - Actors access to the methods working on the state with await; calls within an actor as synchronous and run uninterrupted (even in the case of the extension here below, that has access to the state of the actor):
    extension Counter {
       func resetSlowly(to newValue: Int) {
          value = 0
          for _ in 0..<newValue {
             increment()
          }
          assert(value == newValue)
       }
    }
    
  • 9:00 - Actor reentrancy, i.e. calling actors from asynchronous methods:
    • perform mutation in synchronous code;
    • expect that the actor state could change during suspension;
    • check your assumptions after an await.
    actor ImageDownloader {
       private var cache: [URL: Image] = [:]
    
       func image(from url: URL) async throws -> Image? {
          if let cached = cache[url] {
             return cached
          }
          
          let image = try await downloadImage(from: url)
          
          cache[url] = cache[url, default: image]
          return image
       }
    }
    
    For example in the code here above, we write the cache only if meanwhile no other value has been written.
  • 13:00 - Actor isolation in relation to protocol conformance:
    actor LibraryAccount {
       let idNumber: Int
       var booksOnLoan: [Book] = []
    }
    
    extension LibraryAccount: Equatable {
       static func ==(lhs: LibraryAccount, rhs: LibraryAccount) -> Bool {
          lhs.idNumber == rhs.idNumber
       }
    }
    
    extension LibraryAccount: Hashable {
       nonisolated func hash(into hasher: inout Hasher) {
          hasher.combine(idNumber)
       }
    }
    
    As the method == is static, there is no self instance, and so there is no isolation. While for the hash method, as it is not static, it must be marked as nonisolated to be treated as outside of the actor, even if syntactically it is inside.
  • 15:30 - Actor isolation in relation to closures:
    extension LibraryAccount {
       func readSome(_ book: Book) -> Int { ... }
       
       func read() -> Int {
          booksOnLoan.readuce(0) { book in
             readSome(book)
          }
       }
    }
    
    The closure is itself actor-isolated and thanks to this doesn't require any await.
    extension LibraryAccount {
       func read() -> Int { ... }
       
       func readLater() {
          asyncDetached {
             await read()
          }
       }
    }
    
    As this is async code, it is not in the scope of the actor and because of this is must be awaited.
  • 17:20 - Passing data in and out of actors:
    actor LibraryAccount {
       let idNumber: Int
       var booksOnLoan: [Book] = []
       func selectRandomBook() -> Book? { ... }
    }
    
    class Book {
       var title: String
       var authors: [Author]
    }
    
    func visit(_ account: LibraryAccount) async {
       guard var book = await account.selectRandomBook() else {
          return
       }
       book.title = "\(book.title)!!!"
    }
    
    In this case, Book is a class, and selectRandomBook returns a reference to a mutable object.
    Sendable types are types that are safe to share concurrently, so value types, actors, immutable classes, internally synchronized classes and @Sendable functions.
    Check Sendable by adding a conformance:
    struct Book: Sendable {
       var title: String
       var authors: [Author]
    }
    
    Propagate Sendable by adding a conditional conformance:
    struct Pair<T, U> {
       var first: T
       var second: U
    }
    
    extension Pair: Sendable where T: Sendable, U: Sendable {
    }
    
  • 21:25: @Sendable function types can conform to the @Sendable protocol. @Sendable places restrictions on closures:
    • No mutable captures
    • Captures must be of Sendable type
    • Cannot be both synchronous and actor-isolated
      For example:
    func asyncDetached<T>(_ operation: @Sendable) () async -> T) -> Task.Handle<T, Never>
    
  • 23:35 - The @Main actor is a shortcut to DispatchQueue.main.async.
    @MainActor can be placed on a single function, or on a type (in this case, all methods will be automatically made as main actors). In this case, individual methods can opt-out from the main actor via nonisolated.

Swift concurrency: Update a sample app

  • 5:35 - Code contains no synchronization access or queue, so whatever code makes this thread safe (if there is) must be there somewhere else. But I can't know it just looking at this function - it misses local reasoning.

  • 6:40 - Async code of this function uses delegates for callbacks, that return two values: success and error. We must remember to check that the error is (or not is) nil, and eventually unwrap the optional error. It would be better to throw on errors, but that doesn't work with completion handlers.
    Luckily there is a new save method that doesn't require a completion handler; we call it with await. So we can reorganize this last part as follows:

    do {
       try await store.save(caffeineSample)
       self.logger.debug("\(mgCaffeine) mg Drink saved to HealthKit")
    } catch {
       self.logger.error("Unable to save \(caffeineSample) to the HealthKit store: \(error.localizedDescription)")
    }
    
  • 9:00 - We are calling an asynchronous function from within a synchronous function. One option is to make the sync function async:

    public func save(drink: Drink) async { ... }
    

    But this pushes up the problem one level. We could continue in this way up the chain, but another technique is to spin off a new async task from the parent sync function:

    async { await self.healthKitController.save(drink: Drink) }
    
  • 11:30 - Another technique to convert to async code is to offer alternatives to methods requiring a completion handler:

    public func requestAuthorization(completionHandler: @escaping (Bool) -> Void) { ... }
    

    From the Code menu, we can use the "Add Async Alternative", that marks the
    original code as obsolete and adds a new function:

    @available(*, deprecated, message: "Prefer async alternative instead")
    public func requestAuthorization(completionHandler: @escaping (Bool) -> Void) {
       async {
          let result = await requestAuthorization()
          completionHandler(result)
       }
    }
    
    public func requestAuthorization() async -> Bool {
       // Original code
    }
    
  • 12:50 - Inside the original function, the callback can happen in any arbitrary thread. The assignment to self.isAuthorized is not thread safe. Also we don't have any idea if the next call to the completionHandler is thread safe.
    The async/await version has the same issues. It's not worse, but also it's not better.

  • 15:40 - Another async function conversion:

    public func loadNewDataFromHealthKit(completionHandler: @escaping (Bool) -> Void = { _ in }) { ... }
    

    Gets translated into:

    @available(*, deprecated, message: "Prefer async alternative instead")
    public func loadNewDataFromHealthKit(completionHandler: @escaping (Bool) -> Void = { _ in }) {
       async { completionHandler(await self.loadNewDataFromHealthKit()) }
    }
    
    @discardableResult
    public func loadNewDataFromHealthKit() async -> Bool { ... }
    

    This also uses what is called a continuation:

    public func queryHealthKit() async throws -> (...) {
       return try await withCheckedThrowingContinuation { continuation in
          ...
          
          if let error = error {
             self.logger.error("...")
             continuation.resume(throwing: error)
          } else {
             continuation.resume(returning: ...)
          }
       }
    }
    
  • 21:10 - When we have a completion handler that uses DispatchQueue.main.async. Now we have to introduce Actors and in particular here the MainActor:

    {
       DispatchQueue.main.async {
          self.updateModel(...)
          completionHandler(true)
       }
    }
    

    into:

    {
       await MainActor.run {
          self.updateModel(...)
       }
       return true
    }
    
  • 22:30 - The change above creates a new error, because of possible captured variable. When this happens, we need to make an immutable copy:

    {
       await MainActor.run { [newDrinks] in
          self.updateModel(newDrinks: newDrinks)
       }
       return true
    }
    

    But often it's even better not to have this problem avoiding mutable variables in the first place. For example we declare newDrinks with let instead of var.

  • 24:25 - The current function checks with an assert that it is really running on the main thread:

    private func updateModel(...) {
       assert(Thread.main == Thread.current, "Must be run on the main queue")
    }
    

    But I can forget to do this check. Instead we can annotate functions with MainActor:

    @MainActor
    private func updateModel(...) {
    }
    

    This requires the caller to switch to the main actor before the function is run, and above all the check is done by the compiler.
    Or: we can call a MainActor function outside of the MainQueue, simply awaiting for it (this is the fix suggested by Xcode):

    {
       await self.updateModel(newDrinks: newDrinks)
       return true
    }
    

    But if you are doing multiple calls on the main thread, MainActor.run would group multiple calls together.

  • 28:40 - To protect a full class for concurrency, with @MainActor we protect every method and property for concurrent access. But this synchronizes everything in the main thread.
    So instead we can change the class itself to be an actor:

    actor HealthKitController {
       ...
    }
    

    Differently from the MainActor, this class can be instantiated multiple times.

  • 33:30 - We can mark a function with nonisolated when it doesn't require isolation in the actor, i.e. they don't touch the actor state:

    nonisolated public func requestAuthorization(...) { ... }
    

    In this way they can be called directly from other part of the code.
    Note that the compiler will check that the nonisolated methods don't really touch the state of the actor.

  • 35:00 - Updates in ObservableObject published properties must be done in the main thread. So we could put this class in the MainActor, but there is also a background dispatch queue to perform some work in the background.
    When the majority of the work is performed in the MainActor but there are a few methods that must be run in a background thread, it's a sign to factor this background code into a separate actor:

    private actor CoffeeDataStore {
       private var savedValue: ...
       
       func save(...) { ... }
    }
    
  • 49:40 - UI models on the main actor: we can mark all the model as to be executed in the MainActor. This is particularly ok for ObservableObjects bound to SwiftUI views.

  • 51:35 - Any SwiftUI view that accesses shared state such as EnvironmentObject or an ObservedObject is automatically run in the MainActor.

Meet AsyncSequence

  • 0:30 - Sample usage of AsyncSequence, where we print lines from a web request as they are received, even before the web request is completed:
    let endpointURL = URL(string: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/all_month.csv")
    
    for try await event in endpointURL.lines.dropFirst() {
       let values = event.split(separator: ",")
       ...
    }
    
  • 2:45 - An AsyncSequence can eventually throw an error. It continues to return values, until it returns nil. If it throws, all subsequent values will be nil.
  • 3:40 - The compilation steps to consume the sequence are:
    for await quake in quakes {
       if quake.magnitude > 3 {
          displaySignificantEarthquake(quake)
       }
    }
    
    This gets translated into:
    var iterator = quakes.makeAsyncIterator()
    while let quake = await iterator.next() {
       if quake.magnitude > 3 {
          displaySignificantEarthquake(quake)
       }
    }
    
  • 5:35 - We can exit an AsyncSequence with break, and skip values with continue, and catch errors with try/catch:
    do {
       for try await quake in quakeDownload {
          ...
       }
    } catch {
       ...
    }
    
  • 6:45 - If you need to run iterations in a separate thread, you can create an async task to encapsulate the iteration:
    async {
       for await quake in quakes {
          ...
       }
    }
    
  • 7:25 - In this way, it's also possible to cancel the iteration externally:
    let iteration1 = async {
       for await quake in quakes {
          ...
       }
    }
    let iteration2 = async {
       do {
          for try await quake in quakeDownload {
             ...
          }
       } catch {
          ...
       }
    }
    
    iteration1.cancel()
    iteration2.cancel()
    
  • 7:55 - Main APIs with AsyncAwait available in iOS 15:
    • FileHandle.standardInput.bytes.lines
    • URL(fileURLWithPath: "").lines
    • URLSession.shared.bytes
    • NotificationCenter.default.notifications
  • 11:50 - AsyncStream builds an AsyncSequence over a stream:
    let quakes = AsyncStream(Quake.self) { continuation in
       let monitor = QuakeMonitor()
       monitor.quakeHandler = { quake in
          continuation.yield(quake)
       }
       continuation.onTermination = { _ in
          monitor.stopMonitoring()
       }
       
       monitor.startMonitoring()
    }
    
    let significantQuakes = quakes.filter { quake in
       quake.magnitude > 3
    }
    for await quake in significantQuakes { ... }
    
  • 13:30 - AsyncThrowingStream is like AsyncStream, but it can also handle errors.

Discover concurrency in SwiftUI

  • 1:55 - Data model:
    struct SpacePhoto {
       var title: String
       var description: String
       var date: Date
       var url: URL
    }
    
    extension SpacePhoto: Codable { }
    extension SpacePhoto: Identifiable { }
    
    class Photos: ObservableObject {
       @Published var items: [SpacePhoto]
       
       func updateItems()
    }
    
  • 4:10 - List with plain style and list row separator:
    List {
       ForEach(photo.items) { item in
          PhotoView(photo: item)
             .listRowSeparator(.hidden)
       }
    }
    .navigationTitle("Catalog")
    .listStyle(.plain)
    
  • 8:35 - To work properly, SwiftUI needs that the events are run in order:
    • objectWillChange
    • The state changes
    • The run loop ticks
      If we can do them in the MainActor, we can guarantee that they will be executed in order.
      Now: just use await to yield the main actor.
    @MainActor
    class Photos: ObservableObject {
       ...
       
       func updateItems() async {
          let fetched = await fetchPhotos()
          items = fetched
       }
    
       func fetchPhoto(from url: URL) async -> SpacePhoto? {
          do {
             let (data, _) = try await URLSession.shared.data(from: url)
             return try SpacePhoto(data: data)
          } catch {
             return nil
          }
       }
       
       func fetchPhotos() async -> [SpacePhoto] {
          var downloaded: [SpacePhoto] = []
          for date in randomPhotoDates() {
             let url = SpacePhoto.requestFor(data: date)
             if let photo = await fetchPhoto(from: url) {
                downloaded.append(photo)
             }
          }
          return downloaded
       }
    }
    
  • 13:40: Adding MainActor to a model, ensures that its methods are called from the main actor.
  • 14:05: Instead of using onAppear used in the the past, now there is the new .task modifier, that is invoked when the view is shown and is asynchronous:
    NavigationView {
       ...
    }
    .task {
       await photos.updateItems()
    }
    

15:00: Task is automatically bound to the lifetime of the view, so for example you can await to AsyncSequence, and when the view is dismissed, the task is automatically canceled.
15:20 - There is a new AsyncImage to load images asynchronously from remote servers:

NavigationView {
   AsyncImage(url: photo.url) { image in
      image
         .resizable()
         .aspectRatio(contentMode: .fill)
   } placeholder {
      ProgressView()
   }
   .frame(minWidth: 0, minHeight: 400)
   
   ...
}
.task {
   await photos.updateItems()
}
  • 17:10 - AsyncImage comes with its own defaults, for example in case of errors it continues to show the progress indicator. But it's also possible to customize the error handling behavior.
  • 17:30 - To execute asynchronous actions after a button tap (that is synchronous), we need to start an async task:
    @State private var isSaving = false
    
    Button {
       async {
          isSaving = true
          await photo.save()
          isSaving = false
       }
    } label: {
       Text("Save")
          .opacity(isSaving ? 0 : 1)
          .overlay {
             if isSaving {
                ProgressView()
             }
          }
    }
    .buttonStyle(.bordered)
    .disabled(isSaving)
    

21:00 - The refreshable modifier causes the reload of the data in the list:

List {
   ...
}
.refreshable {
   await photos.updateItems()
}

Use async/await with URLSession

  • 1:05 - Fetch photo sample with completion handlers:
    func fetchPhoto(url: URL, completionHandler: @escaping (UIImage?, Error?) -> Void)
    {
       let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
          if let error = error {
             completionHandler(nil, error)
          }
          if let data = data, let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 {
             DispatchQueue.main.async {
                completionHandler(UIImage(data: data), nil)
             }
          } else {
             completionHandler(nil, DogsError.invalidServerResponse)
          }
       }
       task.resume()
    }
    
  • 2:15 - In the example above, the calls to the completion handlers are not consistently dispatched in the main queue. Also in case of an error, the completion handler can be called twice. Finally, UIImage creation can fail, so we could call the completion handler with both data and error with null value.
  • 3:00 - Here is the same function rewritten using async/await:
    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 DogsError.invalidServerResponse
       }
       
       guard let image = UIImage(data: data) else {
          throw DogsError.unsupportedImage
       }
       
       return image
    }
    
  • 3:50 - Here are the signatures of the methods to retrieve data from the network:
    func data(from url: URL) async throws -> (Data, URLResponse)
    func data(for request: URLRequest) async throws -> (Data, URLResponse)
    
    let (data, response) = try await URLSession.shared.data(from: url)
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 /* OK */ else {
       throw MyNetworkingError.invalidServerResponse
    }
    
  • 4:05 - Here are the upload methods:
    func upload(for request: URLRequest, from Data: Data) async throws -> (Data, URLResponse)
    func upload(for request: URLRequest, fromFile url: Url) async throws -> (Data, URLResponse)
    
    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 /* Created */ else {
       throw MyNetworkingError.invalidServerResponse
    }
    
  • 4:25 - Here are the download methods:
    func download(from url: URL) async throws -> (URL, URLResponse)
    func download(for request: URLRequest) async throws -> (URL, URLResponse)
    func download(resumeFrom resumeData: Data) async throws -> (URL, URLResponse)
    
    let (location, response) = try await URLSession.shared.download(from: url)
    guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 /* OK */ else {
       throw MyNetworkingError.invalidServerResponse
    }
    
    try FileManager.default.moveItem(at: location, to: newLocation)
    
  • 4:45 - Cancellation:
    let handle = async {
       let (data1, response1) = try await URLSession.shared.data(from: url1)
       ...
       let (data2, response2) = try await URLSession.shared.data(from: url2)
       ...
    }
    
    handle.cancel()
    
  • 5:25 - How to receive data incrementally:
    func bytes(from url: URL) async throws -> (URLSession.AsyncBytes, URLResponse)
    func bytes(for request: URLRequest) async throws -> (URLSession.AsyncBytes, URLResponse)
    
    struct AsyncBytes: AsyncSequence {
       typealias Element = UInt8
    }
    
  • 6:50 - Sample method to retrieve data with AsyncSequence:
    func onAppearHandler() async throws {
       let (bytes, response) = try await URLSession.shared.bytes(from: Self.eventStreamURL)
       guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
          throw DogsError.invalidServerResponse
       }
       
       for try await line in bytes.lines {
          let photoMetadata = try JSONDecoder().decode(PhotoMetadata.self, from: Data(line.utf8))
          await updateFavoriteCount(with: photoMetadata)
       }
    }
    
  • 9:20 - URLSessionTask-specific delegate: all the above functions have overloads that receive an additional parameter delegate: URLSessionTaskDelegate?.
  • 10:45 - Sample of authentication with these delegate:
    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.promptForCredentials()
                return (.useCredentials, URLCredentials(user: username, password: password, persistence: .forSession))
             } catch {
                return (.cancelAuthenticationChallenge, nil)
             }
          } else {
             return (.performDefaultHandling, nil)
          }
       }
    }
    
    private func sync() async throws {
       let request = URLRequest(url: endpoint)
       let (data, response) = try await mainURLSession.data(for: request, delegate: AuthenticationDelegate(signInController: signInController))
       guard let response = response as? HTTPURLResponse, response.statusCode == 200 else {
          throw DogsError.invalidServerResponse
       }
       
       let photos = try JSONDecoder().decode([PhotoMetadata].self, from: data)
       
       await updatePhotos(photos)
    }