Protocol implementation of a MainActor delegate requires await keyword on initializer in async code

Why does implementing a delegate that has @MainActor declared on it require the object being created to have an "await" keyword?

Here's an example:

class MyClass : NSObject, UIScrollViewDelegate {
    override init() {
        super.init()
    }
}

class MyClass2 : NSObject {
    override init() {
        super.init()
    }
}

class MyTest : BLTestCase {
    func xtest_getBrandDataFails() async {
        let obj = MyClass().  // expression is async but not marked with await.
        let obj1 = await MyClass()
        let objc2 = MyClass2()
    }
}

UIScrollViewDelegate is declared with @MainActor. And it causes the initialization to require await. That seems odd to me because my init methods aren't marked as async.

It's also odd seeing an await or "suspension point" as stated by the swift concurrency chapter in the swift book, but init is synchronous. Would swift concurrency ever suspend execution here? It seems wrong that it would when I know I don't want a suspension point.

Replies

The short answer here is that it's a bit complicated.

Because of the delegate conformance, MyClass is inferred to be a "global-actor isolated type". In this context, "isolated" means "protected by the actor safety machinery in Swift".

Since you have not specified otherwise, MyClass.init() is an isolated initializer of that global-actor isolated type. Since Swift 5.7, an isolated initializer of an isolated type does not require an await when called, but an isolated initializer of a global-actor isolated type does require an await. The reasons for this are kind of subtle, and they're deeply buried in SE-0327, if you wish to explore all the gory details.

In this case, it looks like you have an "out" — declare the initializer as non-isolated:

class MyClass : NSObject, UIScrollViewDelegate {
    nonisolated override init() {
        super.init()
    }
}

Also, please note that in general you need await to call any isolated method of an actor type, not just async methods. That's because entering an isolated method is a potential suspension point, so the caller needs to be asynchronous too.

  • nonisolated got rid of the error in this case. For some simple cases like unit tests these protections by the swift compiler could be interpreted as barriers or creating additional work. So far swift concurrency seems great. I'm starting to look at writing new code in my app with it, but I really don't like how the @MainActor conformance from a protocol now requires await everywhere. Even for simple properties. Now methods need to be declared for setters. (ug is this java?). Thx for the tip!

Add a Comment