Discover concurrency in SwiftUI

Description: Discover how you can use Swift’s concurrency features to build even better SwiftUI apps. We’ll show you how concurrent workflows interact with your ObservableObjects, and explore how you can use them directly in your SwiftUI views and models. Find out how to use await to make your app run smoothly on the SwiftUI runloop, and learn how to fetch remote images quickly with the AsyncImage API. And we'll take you through the process of enabling additional asynchronous flows in your custom views.

ObservableObject + @MainActor

Imagine a view with the following class as a dependency:

class Photos: ObservableObject { 
  @Published var items: [SpacePhoto] = [] 

  func updateItems() { 
    let fetched = /* fetch new items */ 
    items = fetched 
  }
}

When we assign to items (second row in updateItems()):

  1. an objectWillChange event will trigger
  2. then the new data will be stored in items's storage

When SwiftUI receives an objectWillChange event:

  1. it takes a snapshot of the object (before its storage is updated).
  2. once the storage is updated, SwiftUI compares the previous snapshot with the current value
  3. if the values are different, SwiftUI will update the view (and all other views depending on this Photos object)

This flow works great as long as we are on the main thread/loop. If we assign to items in another thread, both SwiftUI snapshots might be taken before the items storage is actually updated, meaning that the snapshot comparison will find unchanged values, thus not updating our views.

New in Swift 5.5, we can declare our ObservableObject class with swift's @MainActor attribute and, instead of asynchronous callbacks, use the new await syntax to makes sure all operations are executed in the main thread.

New SwiftUI features

  • task(_:) is a new view modifier that lets you run a task for the view lifetime
    • it starts when the view appears (similar to onAppear)
    • it gets cancelled when the view disappears (similar to onDisappear)
  • use AsyncImage to asynchronously load and display an image
AsyncImage(url: photo.url) { image in
  image
    .resizable()
    .aspectRatio(contentMode: .fill)
} placeholder: {
  ProgressView()
}
.frame(minWidth: 0, minHeight: 400)
  • use refreshable(action:) to add pull to refresh capabilities in your views
    • as the action parameter expects an async closure, the refresh indicator stays visible for the duration of the awaited operation

Missing anything? Corrections? Contributions are welcome 😃

Related

Written by

Federico Zanetello

Federico Zanetello

Software engineer with a strong passion for well-written code, thought-out composable architectures, automation, tests, and more.