0

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.

tooberand
  • 78
  • 6
  • 1
    May be you can try and put the refresh logic i.e., call refreshAll() method inside viewWillAppear method of SecondViewController by checking the dirty variable there. Currently you are trying to refresh some view on main thread which is actually not in the UI but to be soon displayed. So better refresh it at the time of displaying. – Teju Amirthi Feb 15 '21 at 19:09
  • Your structure is wrong. The time consuming stuff should not be happening on the main thread. The first `DispatchQueue.main.async` is incorrect. – matt Feb 15 '21 at 19:35
  • [Teju Amirthi](https://stackoverflow.com/users/10175156/teju-amirthi), ```viewWillAppear``` had the same issue, *however*, ```viewDidAppear``` worked like a charm. And I was able to get rid of all the logic in my ```UITabController```. Thank you! – tooberand Feb 15 '21 at 19:38

2 Answers2

0

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.

tooberand
  • 78
  • 6
  • I'm sorry but this is still wrong. You should not be doing something time-consuming on the main thread. It freezes the interface and the Watchdog process can kill your app dead before the user's eyes. – matt Feb 15 '21 at 21:14
0

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()
         }
     })
  }
}
matt
  • 515,959
  • 87
  • 875
  • 1,141
  • matt, thanks for the feedback on my code using the wrong thread. I have changed my implementation to use the ```userInitiated``` quality of service for the background thread. In your opinion, is ```viewDidAppear``` still the right place to call ```refreshAll```? – tooberand Feb 16 '21 at 22:22
  • That depends. Personally I thought your original placement, where `refreshAll` is called only when the user deliberately switches between tabs to this one, made the best sense. – matt Feb 17 '21 at 00:42