swiftasync-awaituikitprotocol-extension

Async/await not supported in protocol extension?


In my iOS application, I have a protocol extension that is similar to the following:

protocol ViewControllerBase: UIViewController {
}

extension ViewControllerBase {
    func showItem(itemID: Int) {
        Task {
            await loadItemDetails(itemID)

            let itemDetailsViewController = ItemDetailsViewController(style: .grouped)

            present(itemDetailsViewController, animated: true)
        }
    }

    func loadItemDetails(_ itemID: Int) async {
    }
}

I'm using a protocol extension so that all of the view controllers that implement ViewControllerBase will inherit the showItem() method. ItemDetailsViewController is defined as follows:

class ItemDetailsViewController: UITableViewController {
}

The call to loadItemDetails() compiles fine, but the calls to the ItemDetailsViewController initializer and present() method produce the following compiler error:

Expression is 'async' but is not marked with 'await'

This doesn't make sense to me, since the initializer and method are not asynchronous. Further, I'm using the same pattern elsewhere without issue. For example, if I convert ViewControllerBase to a class, it compiles fine:

class ViewControllerBase: UIViewController {
    func showItem(itemID: Int) {
        Task {
            await loadItemDetails(itemID)

            let itemDetailsViewController = ItemDetailsViewController(style: .grouped)

            present(itemDetailsViewController, animated: true)
        }
    }

    func loadItemDetails(_ itemID: Int) async {
    }
}

It seems to be related to UIKit specifically, because this code also compiles fine:

class ItemDetails {
}

protocol Base {
}

extension Base {
    func showItem(itemID: Int) {
        Task {
            await loadItemDetails(itemID)

            let itemDetails = ItemDetails()

            present(itemDetails)
        }
    }

    func loadItemDetails(_ itemID: Int) async {
    }

    func present(_ itemDetails: ItemDetails) {
    }
}

Is there a reason this code would not be allowed in a protocol extension, or might this be a bug in the Swift compiler?


Solution

  • This doesn't make sense to me, since the initializer and method are not asynchronous.

    True, but there is another situation in which you have to say await and the target method is treated as async: namely, when there is a cross-actor context switch.

    Here's what I mean.

    What's special about UIKit, as opposed to your ItemDetails / Base example, is that UIKit interface objects / methods are marked @MainActor, requiring that things run on the main actor (meaning, in effect, the main thread).

    But in your protocol extension code, there is nothing that requires anything to run on any particular actor. Therefore, when you call the ItemDetailsViewController initializer or present method, the compiler says to itself:

    "Hmmm, we might not be on the main actor when this code (showItem) runs. So these calls to ItemDetailsViewController methods could require a context switch [changing from one actor to another].)"

    A context switch is exactly when you need to treat the called method as async and say await; and so the compiler enforces that requirement.

    The simple way to make that requirement go away here is to mark your method as @MainActor too. That way, the compiler knows there will be no context switch when this code runs:

    extension ViewControllerBase {
        @MainActor func showItem(itemID: Int) {
            Task {
                await loadItemDetails(itemID)
                let itemDetailsViewController = ItemDetailsViewController(style: .grouped)
                present(itemDetailsViewController, animated: true)
            }
        }
        func loadItemDetails(_ itemID: Int) async {
        }
    }