skip to Main Content

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:

  1. 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).
  2. 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()
            }
         }
      }
   }
  1. 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()
         }
      }
   }
  1. 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


  1. Chosen as BEST ANSWER

    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.


  2. 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()
             }
         })
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search