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?
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.