Using this stackoverflow solution as a guide I have a setup where I have a UITabBarController
and two tabs. When changes are made in the first tab (a UIViewController
), the second tab (another UIViewController
with a UITableView
) needs to perform some calculations, which take a while. So I have a UIActivityIndicatorView
(bundled with a UILabel
) that shows up when the second tab is selected, displayed, and the UITableView
data is being calculated and loaded. It all works as desired in the Simulator, but when I switch to my real device (iPhone X), the calculations occur before the second tab view controller is displayed so there’s just a large pause on the first tab view controller until the calculations are done.
The scary part for me is when I started debugging this with a breakpoint before the DispatchQueue.main.async
call it functioned as desired. So in desperation after hours of research and debugging, I introduced a tenth of a second usleep
before the DispatchQueue.main.async
call. With the usleep
the problem no longer occurred. But I know that a sleep is not the correct solution, so hopefully I can explain everything fully here for some help.
Here’s the flow of the logic:
- The user is in the first tab controller and makes a change which will force the second tab controller to recalculate (via a "dirty" flag variable held in the tab controller).
- The user hits the second tab, which activates this in the
UITabController
:
func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {
let controllerIndex = tabBarController.selectedIndex
if controllerIndex == 1 {
if let controller = tabBarController.viewControllers?[1] as? SecondViewController {
if dirty {
controller.refreshAll()
}
}
}
}
- Since dirty is true,
refreshAll()
is called for the secondController and its implementation is this:
func refreshAll() {
showActivityIndicator()
// WHAT?!?! This usleep call makes the display of the spinner work on real devices (not needed on simulator)
usleep(100000) // One tenth of a second
DispatchQueue.main.async {
// Load new data
self.details = self.calculateDetails()
// Display new data
self.detailTableView.reloadData()
// Clean up the activityView
DispatchQueue.main.async {
self.activityView.removeFromSuperview()
}
}
}
showActivityIndicator()
is implemented in the second view controller as such (activityView is a class property):
func showActivityIndicator() {
let avHeight = 50
let avWidth = 160
let activityLabel = UILabel(frame: CGRect(x: avHeight, y: 0, width: avWidth, height: avHeight))
activityLabel.text = "Calculating"
activityLabel.textColor = UIColor.white
let activityIndicator = UIActivityIndicatorView(style: .medium)
activityIndicator.frame = CGRect(x: 0, y: 0, width: avHeight, height: avHeight)
activityIndicator.color = UIColor.white
activityIndicator.startAnimating()
activityView.frame = CGRect(x: view.frame.midX - CGFloat(avWidth/2), y: view.frame.midY - CGFloat(avHeight/2), width: CGFloat(avWidth), height: CGFloat(avHeight))
activityView.layer.cornerRadius = 10
activityView.layer.masksToBounds = true
activityView.backgroundColor = UIColor.systemIndigo
activityView.addSubview(activityIndicator)
activityView.addSubview(activityLabel)
view.addSubview(activityView)
}
So in summary, the above code works as desired with the usleep
call. Without the usleep
call, calculations are done before the second tab view controller is displayed about 19 times out of 20 (1 in 20 times it does function as desired).
I’m using XCode 12.4, Swift 5, and both the Simulator and my real device are on iOS 14.4.
2
Answers
So the answer is two parts:
Part 1, as guided by matt, is that I was using the wrong thread, which I believe explains the timing issue being fixed by
usleep
. I have since moved to a background thread with a qos ofuserInitiated
. It seems like the original stackoverflow solution I used as a guide is using the wrong thread as well.Part 2, as guided by Teju Amirthi, simplified code by moving the
refreshAll()
call to the second controller'sviewDidAppear
function. This simplified my code by removing the need for the logic implemented in step 2 above in theUITabController
.Your structure is wrong. Time consuming activity must be performed off the main thread. Your
calculateDetails
must be ready to work on a background thread, and should have a completion handler parameter that it calls when the work is done. For example: