To read a file in a document-based Mac app, you subclass NSDocument and implement one of its reading methods. NSDocument is @MainActor, and in the simple case all of its methods are called on the main thread.
But it optionally supports concurrent reading, where documents are opened on background threads, so its reading methods are nonisolated
. As a result, with Strict Concurrency Checking set to "Complete", this code won't compile:
class Content {
func replaceText(with string: String) { ... }
}
class Document: NSDocument {
var content: Content = Content()
// declared as nonisolated on NSDocument
override func read(from data: Data, ofType typeName: String) throws {
// 🛑 "Main actor-isolated property 'content' can not be referenced from a non-isolated context"
content.replaceText(with: String(decoding: data, as: UTF8.self))
}
}
If you don't opt-in to concurrent reading, the solution is simple:
class Document: NSDocument {
override func read(from data: Data, ofType typeName: String) throws {
MainActor.assumeIsolated {
content.replaceText(with: String(decoding: data, as: UTF8.self))
}
}
}
But with concurrent reading, things are more complicated. When you opt-in, each NSDocument is initialized and read on a background thread. Once that's done, ownership is passed to the main thread to show the UI, and the main thread owns the NSDocument going forward. In this case, having MainActor.assumeIsolated
in the body of read(from:ofType:)
no longer makes sense. To wit, if you keep the above code, you get a runtime panic:
class Document: NSDocument {
override class func canConcurrentlyReadDocuments(ofType typeName: String) -> Bool {
true
}
override func read(from data: Data, ofType typeName: String) throws {
MainActor.assumeIsolated { // 💥 Thread 7: Fatal error: Incorrect actor executor assumption; Expected same executor as MainActor.
content.replaceText(with: String(decoding: data, as: UTF8.self))
}
}
}
The truth is that the original version is safe – NSDocument is always isolated to a single thread, just not always the main thread. This is an impedance mismatch between NSDocument and Swift Concurrency.
What's the right way to handle this? I want to tell the compiler "Even though accessing content
from outside MainActor looks unsafe, in this case I know it's fine."