swiftactorquicklookswift-concurrency

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.


Solution

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