Activity Indicator When Switching Tabs - ios

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.

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

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.

Related

Why does a function in my iOS app get called twice?

I made my very first iOS app. But there are two annoying bugs which I cannot get rid of. I hope somebody can help me!
The app is supposed to train to read musical notation. The user specifies his instrument and level (on the previous viewcontroller) and based on that, it places random notes in musical notation on the screen. The user should match those notes in textfields and the app keeps track of the score and advances a level after ten right answers.
However, somehow I'm having problems with the function which generates the random notes. The function for some reason gets called twice, the first time it generates the notes, saves them in a global variable and creates the labels with the notes. The second time, it changes the global variable but not the labels. It returns the following error message this time: 2018-09-29 23:08:37.279170+0200 MyProject[57733:4748212] Warning: Attempt to present <MyProject.ThirdViewController: 0x7fc709125890> on <MyProject.SecondViewController: 0x7fc70900fcd0> whose view is not in the window hierarchy!
Because of this, the user answers the question on the screen, but the app thinks it's the wrong answer, because it has the second answer stored.
The second time the user answers a question, the function is only called once, but the read-out from the text fields doesn't update to the new values, but keeps the same as with the first question.
Here is the code which gives the problems:
import UIKit
class ThirdViewController: UIViewController
{
// snip
func setupLabels() {
// snip
// here the random notes are created, this is function is called multiple times for some reason
let antwoord = Noten()
let antwoordReturn = antwoord.generateNoten(instrument: instrument, ijkpunt: ijkpunt, aantalNoten: aantalNoten-1)
let sleutel = antwoordReturn.0
let heleOpgave = antwoordReturn.1
print(heleOpgave)
print(PassOpgave.shared.category)
let heleOpgaveNummers = antwoordReturn.2
// snip
var a = 0
while a < aantalNoten {
// the labels are created, no problems there
let myTekstveld = UITextField(frame: CGRect(x: labelX, y: labelY + 150, width: labelWidth, height: labelHeight / 2))
myTekstveld.backgroundColor = UIColor.white
myTekstveld.textAlignment = .center
myTekstveld.placeholder = "?"
myTekstveld.keyboardType = UIKeyboardType.default
myTekstveld.borderStyle = UITextField.BorderStyle.line
myTekstveld.autocorrectionType = .no
myTekstveld.returnKeyType = UIReturnKeyType.done
myTekstveld.textColor = UIColor.init(displayP3Red: CGFloat(96.0/255.0), green: CGFloat(35.0/255.0), blue: CGFloat(123.0/255.0), alpha: 1)
myTekstveld.delegate = self as? UITextFieldDelegate
myTekstveld.tag = a + 1
view.addSubview(myTekstveld)
a += 1
labelX += labelWidth
}
// the button is created
}
override func viewDidLoad()
{
super.viewDidLoad()
// snip
setupLabels()
}
#objc func buttonAction(sender: UIButton!) {
// snip
// here the text from the text fields is read, but this only works the first time the buttonAction is called, the next times, it simply returns the first user input.
while a <= aantalNoten {
if let theLabel = view.viewWithTag(a) as? UITextField {
let tekstInput = theLabel.text!
userInput.append(tekstInput)
}
a += 1
}
// snip
setupLabels()
return
}
// snip
You have two instances of ThirdViewController when you don't mean to.
This error is very telling:
2018-09-29 23:08:37.279170+0200 MyProject[57733:4748212] Warning: Attempt to present <MyProject.ThirdViewController: 0x7fc709125890> on <MyProject.SecondViewController: 0x7fc70900fcd0> whose view is not in the window hierarchy!
This is telling you that SecondViewController is trying to create ThirdViewController when SecondViewController is not even on the screen. This suggests that the mistake is in SecondViewController (perhaps observing notifications or other behaviors when not on screen). It's possible of course that you also have two instances of SecondViewController.
I suspect you're trying to build all of this by hand rather than letting Storyboards do the work for you. That's fine, but these kinds of mistakes are a bit more common in that case. The best way to debug this further is to set some breakpoints and carefully check the address of the objects (0x7fc709125890 for example). You'll need to hunt down where you're creating an extra one.
Your genreteNoten method is being called multiple times because it is called from setupLabels which is In turn called from viewDidLoad.
viewDidLoad may be called multiple times and your code should account for that. As it says in this answer to a similar question:
If you have code that only needs to run once for your controller use -awakeFromNib.
I managed to partially solve my second problem myself (that the read-out from the text fields was not updating to the second answer) by not creating them again.
I added some code to setupLabels function to only create the text fields if there was no input already:
let myTekstveld = UITextField()
if (view.viewWithTag(a+1) as? UITextField) != nil {
}
else {
myTekstveld.frame = CGRect(x: labelX, y: labelY + 100, width: labelWidth, height: labelHeight / 2)
// snip
myTekstveld.tag = a + 1
view.addSubview(myTekstveld)
}
The app works as expected now, the only problem is that the text fields are not cleared after each question.

Insert UIView at top of other views programmatically

I work on iOS app, and I need a uiview appear in each uiviewcontroller when there is no internet connection available as in facebook messenger, I write the following code to do that:
extension UIViewController: NetworkStatusListener {
public func networkStatusDidChange(status: Reachability.NetworkStatus) {
switch status {
case .notReachable:
debugPrint("ViewController: Network became unreachable")
case .reachableViaWiFi:
debugPrint("ViewController: Network reachable through WiFi")
case .reachableViaWWAN:
debugPrint("ViewController: Network reachable through Cellular Data")
}
let headerView = UIView(frame: CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 30))
let headerLbl = UILabel()
headerLbl.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: 30)
headerLbl.text = "No Internet Connection!"
headerLbl.backgroundColor = UIColor.red
headerLbl.font = UIFont(name: "Cairo", size: 14)
headerLbl.textAlignment = .center
headerView.addSubview(headerLbl)
//
self.view.insertSubview(headerView, at: 0)
headerView.isHidden = (status == .notReachable)
}
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
ReachabilityManager.shared.addListener(listener: self)
}
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
ReachabilityManager.shared.removeListener(listener: self)
}
}
I depend in checking network availability on this
The checking of network is worked correctly but the desired target isn't achieved.
I need no network connection appear in each uiviewcontroller of my project when there is no internet connection and at top of all other views without overlap other or above them, how can I do that?
a couple of things...
First, in your networkStatusDidChange method, you are creating a new headerView each time - regardless of the network availability status. You might want to a) only create the headerView when you actually want to show it; and b) only create it if you haven't already.
Second, I'm pretty sure inserting the view at index 0 puts it at the bottom. Instead, you could just addSubview the first time around, and then call bringSubview(toFront:) on it when you want to show it again.
If your requirement for having each VC be able to display a status message isn't too strict, you may want to consider an alternate, and the reason I bring this up is that I'm not sure how well this approach plays with creating other views / view controllers - i.e. what happens to the status message when a new view is pushed onto a navigation controller, or the user selects another tab in your app (I have no idea about your app, so I'm just providing examples).
If it makes sense for your app, you could also create a dedicated UIViewController subclass for showing the status (instead of an extension that any view controller can access). Then, set up a view controller hierarchy in your app - for example, if your app is a tab bar app:
AppRootViewController
YourTabBarController
YourNetworkStatusViewController
The net status vc can add/remove its view as needed, in response to network availability changes. The advantage is a separation of concerns - only that one VC knows about showing the network status, and all your other view controllers remain independent from it.
I'd prefer a message extension like SwiftMessages. You can easily call the function from any class/function e. g. your network-availability-function.
in your case it'd look like this:
public func networkStatusDidChange(status: Reachability.NetworkStatus) {
switch status {
case .notReachable:
debugPrint("ViewController: Network became unreachable")
let view = MessageView.viewFromNib(layout: .cardView)
view.configureTheme(.warning)
view.configureDropShadow()
view.configureContent(title: "Warning", body: "Network became unreachable.")
SwiftMessages.show(view: view)
case .reachableViaWiFi:
debugPrint("ViewController: Network reachable through WiFi")
case .reachableViaWWAN:
debugPrint("ViewController: Network reachable through Cellular Data")
}
}
For tableviews you can use some Pods like this: https://github.com/HamzaGhazouani/HGPlaceholders or https://github.com/dzenbot/DZNEmptyDataSet

removeFromSuperview() view Not removed from first time

Ok I have this case where I insert 5 Views programmatically using this method:
let starView = UIImageView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: view.frame.size.width))
// Set Image & Alpha
starView.image = #imageLiteral(resourceName: "star")
starView.alpha = 1
starView.tag = starIndex
// Add to Super View
self.mainView.addSubview(starView)
Please note that starIndex for the 5 views are 1,2,3,4,5 consequently
It's Straightforward.
After a while when an event happens, I use another method to remove these views using this method:
func removeOldStars() {
for index in 1...5 {
if let foundView = view.viewWithTag(index) {
foundView.removeFromSuperview()
}
}
}
What happens here as a result is that the last element only "number 5" is removed. I have tried several trial and error and found this weird behavior. When I remove the view twice using the tag number it works. So for example, if I want to remove view with tag number 3 if I write
view.viewWithTag(3).removeFromSuperView()
view.viewWithTag(3).removeFromSuperView()
It works!!! if just one time it doesn't do anything. I thought maybe the view is added twice and so it need to be removed twice to notice it, but i Debugged it and no the view is added single time.
I removed the view in the main thread to be sure that its not threading issue not no its not the problem.
I would appreciate your help because this is so weird i really need to understand whats happening here.
Tags, in general, are a brittle way to reference views. As #Paulw11 mentioned, this is very likely an issue with other subviews having identical tag values.
In this case, I would hold on to instances of the UIImageViews, and then in the removeOldStars method, iterate through and call removeFromSuperview on the instance directly.
//instantiate empty array of UIImageView
var starViews = [UIImageView]()
//assuming your add method name..
func addStar() {
//your code above up to...
self.mainView.addSubview(starView)
starViews.append(starView)
}
func removeOldStars() {
for view in starViews {
view.removeFromSuperview()
//maybe explicitly de allocate the view depending
}
}

Presented TabBarController disappearing after attempted segue

Short synopsis (XCode 7.2, Swift2, iOS 9.2 as target):
1) In a first.storyboard, I have a single viewController.
2) In a second.storyboard, I have a tabbarController, with multiple navigationControllers with tableviewControllers (see attached image). Also of note is when second.storyboard is the one used on launch, everything works correctly.
3) the main UI for the app is in the first.storyboard, and I want to present the tabbarcontroller in the second.storyboard
4) No matter which way I present it (storyboard reference/segue, presentViewController, showViewController), the tabbarcontroller and all the initial views work, but if I tap a tableviewcell to segue to another view, the whole tabbarcontroller and contents disappear, leaving me back at the viewcontroller in first.storyboard.
I can cheat, and set the rootViewController manually and things seem to work
let sb = UIStoryboard(name: "second", bundle: nil)
let navController = sb.instantiateViewControllerWithIdentifier("secondIdentifier") as! UITabBarController
UIApplication.sharedApplication().keyWindow?.rootViewController = navController
And I suspect I can add an animation to this to not have the transition not be so stark. But this seems like something I shouldn't have to do, and kind of a pain to troubleshoot in the future. Am I missing something fundamental in making this work?
EDIT: Video of it not working https://youtu.be/MIhR4TVd7CY
NOTE: The last app I made originally targeted iOS4, and I did all the views programatically. It seemed like all the updates to IB and segues etc would make life more manageable (and for the most part that has been true), but this is still my first foray in to it, so I may be missing some important points of information to describe the issue.
I have found a superior way to deal with this: UIViewControllerTransitioningDelegate
It's a bit of extra work to implement, but it produces a "more correct" result.
My solution was to make a custom UIStoryboardSegue that will do the animation as well as set the rootViewController.
import UIKit
class changeRootVCSeguePushUp: UIStoryboardSegue {
override func perform() {
let applicationDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
let sourceView = self.sourceViewController.view
let destinationView = self.destinationViewController.view
let sourceFrame = sourceView.frame
let destinationStartFrame = CGRect(x: 0, y: sourceFrame.height, width: sourceFrame.width, height: sourceFrame.height)
let destinationEndFrame = CGRect(x: 0, y: 0, width: sourceFrame.width, height: sourceFrame.height)
destinationView.frame = destinationStartFrame
applicationDelegate.window?.insertSubview(self.destinationViewController.view, aboveSubview: self.sourceViewController.view )
UIView.animateWithDuration(0.25, animations: {
destinationView.frame = destinationEndFrame
}, completion: {(finished: Bool) -> Void in
self.sourceViewController.view.removeFromSuperview()
applicationDelegate.window?.rootViewController = self.destinationViewController
})
}
}
I could not find a way in interface builder, or in code other than changing the rootViewController to get this working. I would end up with various random navigation issue like overlapping navigation bars, segue animations not working correctly until I changed tabs, full on lockups with no information in the console, etc.
I have previously presented a tabBarcontroller modally (without changing rootviewController), but everything was done in code (working as of ios7 and objective-c). No clue what is going on under the covers when the view hierarchies are made in a storyboard, but wondering if this is perhaps a bug.
Thanks to multiple other answers here on stackoverflow to get to mine!

iOS Custom Segue Not Working

I am just starting to learn how to develop iOS apps. (Note that I am using XCode 6 beta and Swift) I think I'm good with building interfaces themselves, but I seem to be having trouble with segues.
The first page of my app is just a simple screen with a sign up button and a sign in button. I made a separate view controller for a sign up page and put a text field on it. Both of these view controllers are in my storyboard. I then made a custom segue class to simply animate the start page and the sign up page moving one screen to the left, so that the sign up page is now showing. This is my custom segue class:
class SlideFromRightSegue: UIStoryboardSegue {
override func perform() {
let screenWidth = UIScreen.mainScreen().bounds.width
let center = self.sourceViewController.view!.center
UIView.animateWithDuration(0.25,
animations: {
self.sourceViewController.view!.center = CGPoint(x: center.x - screenWidth, y: center.y)
self.destinationViewController.view!.center = CGPoint(x: center.x - screenWidth, y: center.y)
}, completion: { (finished: Bool) in
self.sourceViewController.presentViewController(self.destinationViewController as UIViewController, animated: false, completion: nil)
})
}
}
I based this off a tutorial I found, and I don't think there is a problem here. I simply animate both the source and destination VC's center to be one screen to the left. I'm not entirely sure how animations work in iOS but the tutorial I found was structured similarly, so I assume that the closure passed into the "animations" parameter is called and then iOS generates all of the steps in between and draws each frame to the screen. (correct me if I'm wrong)
So I have a custom segue. I go back to my storyboard, click on the sign up button, go over to the connections tab, and drag the "Triggered Segues - action" over to the second VC, select "custom" and then select my custom class. For some reason, instead of being just "SlideFromRightSegue", it's "_TtC524SlideFromRightSegue". I'm not sure why that's happening, but I assume it has something to do with the fact that I renamed the segue class, and I hope that's not causing any problems. Anyway, doing that creates an arrow from the first VC to the second VC, so I assume it worked.
When I run the app, I get the start screen just like usual. I click the sign up button, and nothing happens. There is probably something I missed, but I put a breakpoint in the perform() function of my segue and another in the prepareForSegue() function of the first VC. Neither one was triggered. I think I have the segue set up properly, I just have something wrong with the actual implementation into my app. Anybody have any idea what's going on?
So yea, it turns out that the problem was that I was using a custom button class as well, which had overrides for the touchesBegan, touchesEnded, and touchesCancelled functions. Those overrides did not callback to the superclass methods, so the action of pressing the button wasn't even being triggered.
I guess the lesson I can take from all this is to make sure to know which function overrides must call back to the superclass, and which should completely override the superclass. Touch event handlers likely need to call back to the superclass.
I don't know why your perform method wasn't called, but it's not due to your code, though the code does have some problems. You never add the destination view controller's view to the window (until the completion where the presentation will do that for you). So, you need to add the view, position it off-screen right, and then have it end up centered, not off screen left like you have in your code,
class SlideFromRightSegue: UIStoryboardSegue {
override func perform() {
let screenWidth = UIScreen.mainScreen().bounds.width
let center = self.sourceViewController.view!.center
let appdel: AppDelegate = UIApplication.sharedApplication().delegate as AppDelegate
appdel.window!.addSubview(self.destinationViewController.view)
self.destinationViewController.view!.center = CGPoint(x: center.x + screenWidth, y: center.y)
UIView.animateWithDuration(0.25,
animations: {
self.sourceViewController.view!.center = CGPoint(x: center.x - screenWidth, y: center.y)
self.destinationViewController.view!.center = center
}, completion: { (finished: Bool) in
self.sourceViewController.presentViewController(self.destinationViewController as UIViewController, animated: false, completion: nil)
self.sourceViewController.view!.removeFromSuperview()
})
}
}

Resources