Search code examples
swiftconcurrency

How to make overridden methods run on @MainActor?


I am trying to refactor my existing implementation for an app so it conforms to Swift 6. To do this I have to touch the following class that provides an "open url in safari" share extension:

import Foundation
import UIKit

public final class SafariActivity: UIActivity {

    var url: URL?
    
    // MARK: - UIActivity Implementation
    
    public override var activityType: UIActivity.ActivityType {
        .openInSafari
    }
    
    public override var activityTitle: String? {
        "Open in Safari"
    }
    
    public override var activityImage: UIImage? {
        UIImage(systemName: "safari")?.applyingSymbolConfiguration(.init(scale: .large))
    }
    
    public override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
        // ERROR: Main actor-isolated class property 'shared' can not be referenced from a nonisolated context
        activityItems.contains { $0 is URL ? UIApplication.shared.canOpenURL($0 as! URL) : false }
    }
    
    public override func prepare(withActivityItems activityItems: [Any]) {
        // ERROR: Main actor-isolated class property 'shared' can not be referenced from a nonisolated context
        url = activityItems.first { $0 is URL ? UIApplication.shared.canOpenURL($0 as! URL) : false } as? URL
    }
    
    public override func perform() {
        if let url = url {
            // ERROR: Call to main actor-isolated instance method 'open(_:options:completionHandler:)' in a synchronous nonisolated context
            UIApplication.shared.open(url)
        }
        self.activityDidFinish(true)
    }
}

extension UIActivity.ActivityType {
    static let openInSafari = UIActivity.ActivityType(rawValue: "openInSafari")
}

extension UIActivity {
    @MainActor public static let openInSafari = SafariActivity()
}

I struggle to understand the correct way to restrict methods that are overrides to the main thread. How can I make the methods I have overridden from UIActivity isolate to @MainActor?


Solution

  • The main problem here is that UIApplication.shared is main actor-isolated. canOpenURL is non-isolated, so there's no problem there.

    If you just grab UIApplication.shared when you know you are in a main actor-isolated context, you can store it in a property and use it.

    For example:

    var url: URL?
    let sharedApp: UIApplication
    
    override init() {
        // make sure to always call SafariActivity.init in a main actor isolated context!
        sharedApp = MainActor.assumeIsolated { UIApplication.shared }
    }
    

    Unfortunately we cannot mark init as @MainActor because it would override NSObject.init, which is non-isolated. If this is directly inheriting from NSObject, then Swift can know this is safe, but we are inheriting UIActivity, so Swift can't tell.

    You can also add an extra parameter to take in the UIApplication, so the init doesn't override NSObject.init,

    @MainActor
    init(_ app: UIApplication) {
        sharedApp = app
    }
    // usage:
    SafariActivity(.shared)
    

    Then, you can just replace all the UIApplication.shared with sharedApp.

    Finally, despite not being marked @MainActor, perform is actually documented to only be called on the main thread, so it is safe to just use MainActor.assumeIsolated.

    public override func perform() {
        if let url = url {
            MainActor.assumeIsolated {
                UIApplication.shared.open(url)
            }
        }
        self.activityDidFinish(true)
    }