Search code examples
swiftconcurrency

Options to init with @MainActor property as default value


I have a class which up until now used the UIApplication.shared as a default argument during it's initialisation. The simplified version of it:

final class MyClass {
    private var application: UIApplication
    
    init(application: UIApplication = UIApplication.shared) {
        self.application = application
    }
}

With Strict Concurrency Checking using the Complete option now gives the following error:

Main actor-isolated class property 'shared' can not be referenced from a non-isolated context

It makes sense since UIApplication is marked as @MainActor hence, all it's properties are async:

@available(iOS 2.0, *)
@MainActor open class UIApplication : UIResponder {
open class var shared: UIApplication { get }

...
}

I see two solutions for this:

#Solution1

Mark the property as optional and initialise it during the init. In some cases it may seem as a good idea, but with this we loose the ability to dependency inject the property and make it testable. Also using optionals further when we know the property is indeed initialised is uncomfortable:

final class MyClass {
    private var application: UIApplication?
    
    @MainActor
    init() {
        self.application = UIApplication.shared
    }
}

#Solution2

Let's make the caller responsible for initialising the class on the @Mainactor:

final class MyClass {
    private var application: UIApplication
    
    init(application: UIApplication) {
        self.application = application
    }
    
    // Just to present what the caller would do
    @MainActor
    func option1ToInit() {
       _ = MyClass(application: UIApplication.shared)
    }
    
    // Just to present what the caller would do
    func option2ToInit() {
        Task {
            _ = await MyClass(application: UIApplication.shared)
        }
    }
}

The second solution seems to work nicely, however I need to do a bit of refactor work so all the callers are executing on the @MainActor. I was wondering if there is any other option I am missing here.


Solution

  • If you can add @MainActor to init (which it seems you can from your description), then you just need to move the accessing of .shared into the init:

    @MainActor
    init(application: UIApplication? = nil) {
        self.application = application ?? .shared
    }
    

    UPDATE: SE-0411 fixes this limitation. You can enable it in Swift 5.10 using the upcoming feature IsolatedDefaultValues.