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