1

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?

Greg Brown
  • 3,168
  • 1
  • 27
  • 37

1 Answers1

1

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 {
    }
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Thank you, I will try that. But why is that required for the protocol implementation, and only for UIKit-derived types? – Greg Brown Mar 13 '22 at 00:21
  • 1
    The compiler knows that the ItemDetailsViewController initializer and `present` methods will run on the main thread. But it has no way of knowing what thread type (main or non-main) _this_ function will run on unless you tell it. And if this function were to run on a non-main thread, we'd need a context switch (`await`). So the compiler assumes that the context switch might be needed — unless you assure it otherwise. – matt Mar 13 '22 at 02:44
  • Makes sense. Adding `@MainActor` does indeed resolve the problem. Thanks! – Greg Brown Mar 13 '22 at 12:51
  • 1
    I'll add the info from that comment to the actual answer, so that it stands a better chance of helping others. – matt Mar 13 '22 at 15:35