2

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()
    }
  }
cvld
  • 77
  • 1
  • 6
  • 2
    I doubt that an `@objc` target/action method can be async. – vadian Feb 13 '22 at 11:34
  • I just realized the same thing right now. If you submit your answer I will accept it – cvld Feb 13 '22 at 11:52
  • @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. – clearlight Apr 08 '22 at 13:39
  • 1
    @clearlight Target/action belongs to Objective-C (note the `@objc` attribute) and Objective-C has no clue about Swift concurrency. – vadian Apr 08 '22 at 14:13

2 Answers2

2

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
)
aclima
  • 630
  • 1
  • 11
  • 20
1

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

cvld
  • 77
  • 1
  • 6