Bryan
Bryan

Reputation: 5779

Swift: Conform Older Protocols to MainActor Isolation?

Context

I have a Mac app that uses the old QuickLook protocols: QLPreviewPanelDataSource and QLPreviewPanelDelegate. You select a row in an NSTableView, hit the spacebar, and you get the QuickLook preview. Standard stuff:

@MainActor
final class SomeController: NSTableViewDataSource, QLPreviewPanelDataSource
{
    private var tableView: NSTableView

    func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
       return tableView.numberOfSelectedRows
    }
}

Problem

I'm adopting Actors and Swift Concurrency in this app. SomeController is now assigned to @MainActor as it should be, since it controls UI. But this brings warnings for the -numberOfPreviewItems() implementation:

Main actor-isolated instance method 'numberOfPreviewItems(in:)' cannot be used to satisfy nonisolated protocol requirement

QLPreviewPanelDataSource is not decorated with @MainActor, which would obviously solve the problem. I cannot simply mark the function nonisolated, since it touches tableView, which IS isolated to the Main Actor. And I cannot await an access, since the protocol method does not support concurrency.

Everything works fine, of course, but having 30-some spurious build warnings is a giant distraction.

Question

What is the correct way to silence these warnings assuming:

  1. Apple will never update the protocols with the @MainActor decoration. (Radars to do so are unanswered for years.)

  2. I want to keep the Strict Concurrency Checking build setting set to complete to catch other, legitimate issues.

Upvotes: 10

Views: 2827

Answers (3)

Jacob Wallström
Jacob Wallström

Reputation: 163

So using MainActor.assumeIsolated caused crashes for me because the callback wasn't coming on the main queue. Tested on IOS 18 Iphone and Xcode 16.

This worked however:

@MainActor
final class Coordinator: NSObject, ARSCNViewDelegate, Sendable {
    nonisolated func renderer(_: any SCNSceneRenderer, nodeFor _: ARAnchor) -> SCNNode? {
        DispatchQueue.main.sync {
            SCNNode()
        }
    }

}

Upvotes: 0

Brianna Doubt
Brianna Doubt

Reputation: 709

Xcode 16 Update:

For this answer to work in Xcode 16 with Swift 6 Language Mode, it is now necessary to decorate the import statement:

@preconcurrency import Quartz

In older versions of Xcode, the @preconcurrency decorator was non-functional, but that appears to have changed in Xcode 16. Without this decorator, MainActor.assumeIsolated will produce Sendable errors.

Original Answer:

As you’ve pointed out, Apple has not isolated some legacy Objective-C classes, and given the requests have been open a while, it doesn’t seem like they will. Given this, it doesn’t seem possible to isolate an entire object without access to the object or protocol definition.

To address this isolation issue, Apple has provided this. I found this after running across this question when I had a similar issue

MainActor.assumeIsolated {}

I was building an ARKit feature and needed to conform to ARSCNViewDelegate, which is locked to the main thread in Objective-C, but considered nonisolated when it came to Concurrency. Since the delegate method returns a value, I couldn't wrap the body in a Task.

This is what worked for me:

@MainActor
class MyViewController: UIViewController, ARSCNViewDelegate 
{
    // MARK: ARSCNViewDelegate methods

    nonisolated func renderer(_ renderer: SCNSceneRenderer, nodeFor anchor: ARAnchor) -> SCNNode? {
        return MainActor.assumeIsolated {
            // Assumed to be isolated to MainActor
            let node = SCNNode() // This line refuses to compile if not isolated since `SCNNode` is isolated to the MainActor.
            // Configure node and return.
            return node
        }
    }
}

No warnings, no errors.

Upvotes: 6

CouchDeveloper
CouchDeveloper

Reputation: 19174

As Brianna Doubt already figured out in her answer (do not credit me!), one can use MainActor\assumeIsolated(_:file:line:) to silence the the compiler warnings:

In your main actor isolated class, define the delegate function as follows:

    nonisolated
    func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
        assert(Thread.isMainThread)
        MainActor.assumeIsolated {
            return tableView.numberOfSelectedRows
        }
    }

The code above makes the assumption, that the delegate is called on the main thread. Since we cannot control the caller of the delegate, we cannot guarantee that this is actually the case. But at least, we can test it.

The assert(Thread.isMainThread) will catch any violation of our assumption in Debug configuration. However, assumeIsolated(...) will crash anyway if the code will not run on the main thread. So, the explicit assert is more for documentation making it double clear what we assume.

Then, MainActor.assumeIsolated(..) will silence the warnings.

Consider this as a workaround. Actually, Apple should address this issue and make it conform the concurrency rules.

Side notes:

You possibly do not need to make the whole class MyViewController isolated to the main actor. Possibly, there are other workarounds in your code.

Regarding your statement: "Apple will never update the protocols with the @MainActor decoration":

Possibly, they can't due to breaking APIs.

The other reason could be, that Apple don't want to isolate the delegate to the main actor, which makes sense for "data oriented" APIs. Either way, we don't know.

Upvotes: 1

Related Questions