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:
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()
}
}
}
}
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.
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 of userInitiated
. 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's viewDidAppear
function. This simplified my code by removing the need for the logic implemented in step 2 above in the UITabController
.
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:
func refreshAll() {
showActivityIndicator()
myBackgroundQueue.async {
self.calculateDetails(completion: {
DispatchQueue.main.async {
self.detailTableView.reloadData()
self.activityView.removeFromSuperview()
}
})
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.