swiftswiftuiswift5

Is it safe to mutate @State variables from inside an async block in SwiftUI?


I have the following struct (View) that uses async/await internally.

struct CachedAsyncImage<I: View, P: View>: View {
    @State var image: UIImage?
    
    let content: (Image) -> I
    let placeholder: () -> P
    let url: URL?
    
    public init(
        url: URL?,
        @ViewBuilder content: @escaping (Image) -> I,
        @ViewBuilder placeholder: @escaping () -> P
    ) {
        self.url = url
        self.content = content
        self.placeholder = placeholder
    }
    
    var body: some View {
        async {
            guard let url = self.url else {
                return
            }
            
            let session = URLSession.shared
            let taskResponse = try? await session.data(from: url, delegate: nil)
            
            guard let imageData = taskResponse?.0,
                  let uiImage = UIImage(data: imageData) else {
                      return
                  }
            
            image = uiImage
        }
        
        guard let image = self.image else {
            return AnyView(placeholder())
        }
        
        return AnyView(content(Image(uiImage: image)))
    }
}

The only way I made the internal views (I and P types) to swap accordingly was to assign the image property, which had to be marked with the @State property wrapper, from inside the async block. Is this a safe practice from a threading/thread-safety perspective or do I have to implement it in some other way?


Solution

  • This answer is no longer relevant, please check the Malhal's post for an up-to-date answer.


    It is not safe to update UI data, from another thread than the main one. And Swift's concurrency model makes no guarantees about the thread on which async code runs.

    So the answer to your question is a big NO.

    You will need to dispatch the property update on the main thread. And the main actor is the recommended way to do this.

    Just add a new function for this

    @MainActor func updateImage(to newImage: UIImage) {
        image = newImage
    }
    

    , and call it from the async task:

    await updateImage(to: uiImage)