Swift 5.5: Async @objc didPullToRefresh selector crashes app with error EXC_BAD_ACCESS
Asked Answered
M

2

3

I have a table to which I've added a refreshControl and when I pull down to refresh the content, I reset the array that feeds the table with data and then immediately request new data through an API call.

Until now, I have used completion handlers and protocols to get the data into the table view but I want to move the logic to async/await because of the complexity needed by the network calls and the pyramid of nested closures.

Populating the view in viewDidLoad works fine but with pullToRefresh selector I get an error:

Thread 1: EXC_BAD_ACCESS (code=1, address=0xbcf917df8160)

Implementation:

override func viewDidLoad() {
    super.viewDidLoad()
    setupView()
    setupTableView()
    setupTableRefreshControl()
    Task {
      await getBalances() //async network call
      myTable.reloadData()
    }
  } 
func setupTableRefreshControl() {
    myTable.refreshControl = UIRefreshControl()
    myTable.refreshControl?.addTarget(self, action: #selector(didPullToRefresh), for: .valueChanged)
  }

Code that crashes app:

   @objc func didPullToRefresh() async {
    balance.reset() // reset array to []
    Task {
      await getBalances() //async network call
      myTable.reloadData()
    }
  }
Munmro answered 13/2, 2022 at 11:17 Comment(4)
I doubt that an @objc target/action method can be async.Megganmeggi
I just realized the same thing right now. If you submit your answer I will accept itMunmro
@vadian, why isn't that documented? I wonder if it is a bug or something that will be fixed? The problem is if you try to call an async function from an action handler that isn't async, the compiler seems to expect async keyword to be added to the action handler itself, and it's quite a limitation if there's no way in any part of a button response to call an async function. There has to be someway to decouple the event.Camarena
@Camarena Target/action belongs to Objective-C (note the @objc attribute) and Objective-C has no clue about Swift concurrency.Megganmeggi
W
7

At the time of writing, @objc selectors and async methods don't play well together and can result in runtime crashes, instead of an error at compile-time.

Here's a sample of how easy it is to inadvertently replicate this issue while converting our code to async/await: we mark the following method as async

@objc func myFunction() async {
    //...

not noticing that it is also marked as @objc and used as a selector

NotificationCenter.default.addObserver(
    self,
    selector: #selector(myFunction),
    name: "myNotification",
    object: nil
)

while somewhere else, a notification is posted

NotificationCenter.default.post(name: "myNotification", object: nil)

Boom 💥 EXC_BAD_ACCESS

Instead, we should provide a wrapper selector for our brand new async method

@objc
func myFunctionSelector() {
    Task {
        await myFunction()
    }
}

func myFunction() async { 
    //... 

and use it for the selector

NotificationCenter.default.addObserver(
    self,
    selector: #selector(myFunctionSelector),
    name: "myNotification",
    object: nil
)
Westberry answered 20/4, 2023 at 17:59 Comment(1)
actually seems like a bug then. The compiler should see when #selector is used together with an async method. I just tried and I can stil build this kind of codeGilemette
M
1

I realized that didPullToRefresh does not need to be marked as async. Removing "async" from the function signature solves the problem

Munmro answered 13/2, 2022 at 11:55 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.