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()
}
}
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
)