Search code examples
iosswift

Capture of 'self' with non-sendable type 'TypeName' in a `@Sendable` closure


I have an API class in which I am creating a URL session data task that updates the value of a birdResult property in the class when the task completes from within a DispatchQueue.main.async block.

In Swift 6, the following error appears: enter image description here

My class code looks like this:

class BirdsAPI: ObservableObject {
    static let url = URL(string: "https://api.inaturalist.org/v1/observations?iconic_taxa=Aves&per_page=50&order=desc&order_by=created_at")
    
    @Published var birdResults: [BirdResult] = []
    
    func fetchObservations() {
        URLSession.shared.dataTask(with: URLRequest(url: BirdsAPI.url!)) { data, response, error in
            guard error == nil else { return }
            
            let decoder = JSONDecoder()
            
            do {
                let body = try decoder.decode(Body.self, from: data!)
                DispatchQueue.main.async {
                    self.birdResults = body.results
                }
            } catch let error {
                print("error decoding data, \(error)")
            }
        }.resume()
    }
}

Why is this and how can I fix it?


Solution

  • @Senable describes the types that are safe to share concurrently. i.e:

    struct A: Senable { }    //✅
    
    class B: Senable { }     //❌ unsynchronized
    
    final class C: Senable { //❌ mutating property
        var birdResults: [BirdResult] = []
    }  
    
    final class D: Senable {  //✅
        let birdResults: [BirdResult]
    }
    

    The purpose is to avoid creating the risk of data races. In your example, your BirdsAPI class does not conform to Senable, but the completionHandler is marked with @Sendable:

    func dataTask( with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void ) -> URLSessionDataTask

    Although you may disregard the error from Swift 6 using @unchecked. You should not do this unless you have some internal locking mechanism in place when modifying the property.

    class BirdsAPI: ObservableObject, @unchecked Sendable { } //<- here
    

    Thus, I recommend refactoring the code to the following and avoiding combining the DispatchQueue family with the new concurrency async await.

    @MainActor
    func fetchObservations() async throws {
        let (data, response) = try await URLSession.shared.data(for: URLRequest(url: BirdsAPI.url!))
        let decoder = JSONDecoder()
        let body = try decoder.decode(Body.self, from: data!)
        self.birdResults = body.results
    }