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
?
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)
}