iosswiftnsoperationqueueoperationoperationqueue

iOS custom Operation for Operation Queue gives warning Class must restate inherited '@unchecked Sendable' conformance


I am using Xcode 16.2.

I have the below Operation:

import Foundation
import UIKit

typealias ParsingCompletionHandler = ((ParsedRecord) -> ())

class RecordParseOperation: Operation {//THIS GIVES WARNING: Class 'RecordParseOperation' must restate inherited '@unchecked Sendable' conformance
    var result: ParsedRecord?
    var parsingCompleteHandler: ParsingCompletionHandler?
    private var record: Record
    private var indexPath: IndexPath
    
    init(record: Record, indexPath : IndexPath) {
        self.record = record
        self.indexPath = indexPath
        super.init()
    }
    
    override func main() {
        
        if Thread.isMainThread {
            print("⚠️ Main thread! \(indexPath)")
        }
        
        if isCancelled { return }
        
        let titleFont = UIFont.systemFont(ofSize: 18, weight: .semibold)
        
        let attributedTitle = NSMutableAttributedString(string: "\(indexPath.row+1). ", attributes: [.font:titleFont,.foregroundColor:UIColor.secondaryLabel])
        
        if isCancelled { return }
        attributedTitle.append(NSAttributedString(string: record.title, attributes: [.font:titleFont,.foregroundColor:UIColor.label]))
        
        if isCancelled { return }
        
        let bodyFont = UIFont.systemFont(ofSize: 14, weight: .regular)
        let attributedBody = NSMutableAttributedString(string: record.body+"\n\n", attributes: [.font:bodyFont, .foregroundColor:UIColor.label])
        
        if isCancelled { return }
        attributedBody.append(NSAttributedString(string: record.link, attributes: [.font:bodyFont, .link:record.link]))
                        
        if isCancelled { return }
        
        let result = ParsedRecord(attributedTitle: attributedTitle, attributedBody: attributedBody)
        self.result = result
        
        parsingCompleteHandler?(result)
    }
}

The above code gives warning at the class RecordParseOperation: Operation { line:

Class 'RecordParseOperation' must restate inherited '@unchecked Sendable' conformance

How can I fix this?


Solution

  • You can do what it suggests, namely add @unchecked Sendable conformance.

    But when you do that, you are entering a contract ensures that you will implement thread-safe access to any mutable state. In short, you must implement this thread-safety yourself.

    So, perhaps hide the backing variables (with the _ prefix) and expose a computed property that uses a lock, e.g., a OSAllocatedUnfairLock, to synchronize access:

    import os.lock
    
    struct Record: Sendable { … }
    
    struct ParsedRecord: Sendable { … }
    
    typealias ParsingCompletionHandler = @Sendable (ParsedRecord) -> ()
    
    class RecordParseOperation: Operation, @unchecked Sendable {
        private let lock = OSAllocatedUnfairLock()
    
        private var _result: ParsedRecord?
        var result: ParsedRecord? {
            get { lock.withLock { _result } }
            set { lock.withLock { _result = newValue } }
        }
    
        private var _parsingCompleteHandler: ParsingCompletionHandler?
        var parsingCompleteHandler: ParsingCompletionHandler? {
            get { lock.withLock { _parsingCompleteHandler } }
            set { lock.withLock { _parsingCompleteHandler = newValue } }
        }
    
        private let record: Record
        private let indexPath: IndexPath
    
        init(record: Record, indexPath: IndexPath) {
            self.record = record
            self.indexPath = indexPath
            super.init()
        }
    
        override func main() {
            …
        }
    }
    

    (Or nowadays I’d often use a Mutex in the Synchronization library, but the idea is the same.)

    Bottom line, to satisfy the @unchecked Sendable requirement, you must synchronize access to any mutable state.


    The other approach is to ask yourself whether you really need this mutable state that this subclass has introduced. E.g., the parsingCompleteHandler could be an immutable type (e.g., a let), that you set in the initializer of the Operation subclass. Likewise, given that you have parsingCompleteHandler that returns the result, do you really need a result mutable variable? Can’t you just rely on this completion handler closure to return the result? That eliminates races on this mutating result property.

    If you get rid of these two mutable properties, that eliminates the need to manually implement your own synchronization of them. It is another approach.