How can a user change tabs without directly tapping the tab? - ios

I've been having a very tricky bug where after a significant amount of time on the device and simulator (it's unclear when), tapping anywhere on the screen brings me back to the very first/top tab in the tab bar's tabs. The rough layout for the app is below:
For the first/top tab, the user is taken through three scenes, and after completing the third the last two are popped off and the user is returned to the root Scene (the first in the Navigation Controller's stack). I can confirm that there are no memory leaks, at least regarding these controllers. The latter two are popped off and are clearly de-allocated.
In the second/middle tab, the controller holds a container view that holds a UIPageViewController with several controllers.
The third tab is the most straightforward and is a TableViewController with a single detail view.
All three tabs in some way reference the base tab bar controller, which is where I have put a common store object that each of the tabs may read/write to.
It does not make sense in the app to press the first tab to clear the cyclic Scene, so I have implemented a check in the base UITabBarController that blocks against clearing it when already at that tab and returning true otherwise:
func tabBarController(
_ tabBarController: UITabBarController,
shouldSelect viewController: UIViewController
) -> Bool {
guard let vc = selectedViewController else {
fatalError(
"TabBarController should always have a non-nil selectedViewController.")
}
var result = true
if let currentVC = vc as? UINavigationController {
// Block clearing to the base Controller in the Cycle if already in the Cycle.
if let cvc = currentVC.viewControllers[0] as? CyclicViewController
{
let areEqual = viewController.isEqual(currentVC as UIViewController)
result = !areEqual
}
}
return result
}
As stated, after a certain amount of time (usually after at least one or two iterations of the cyclic View Controllers in the first tab, although sometimes at launch as well), the app eventually and permanently starts to return to the the first tab when tapping the screen, regardless of which controller or where you tap. You can still tap back to the other tabs, but tapping anywhere brings you back to the first, even when tapping on the current tab. It's like the first tab hasn't really been switched out, even though it clearly does as the above method is called.
I can confirm that the above shouldSelect is called with these unintentional tab switches, but this is the only code that ever talks about switching between tabs. Nowhere else in the codebase is there code that says to switch to another tab, conditionally or otherwise.
There is no threaded code anywhere in my codebase. Everything runs on the main thread.
The selectedViewController property is assigned to the View Controller at first/top tab in the viewDidLoad of the Tab Bar Controller.
I would like to be able to reproduce this, but can't seem to, or else I'd have used a simpler example. I'm aware I'm not showing much, but I'd rather not show more than what I have. Setting break points is a huge pain, even with a generous limit - I don't know how to reliably reproduce this error, so it's very difficult to put a number and the breakpoint is very annoying to use.
Regarding tap gesture recognizers - the middle tab with its PageViewController does page through several ViewControllers with objects that have tap gesture recognizers. I can confirm with grep -R "ecognizer" in my workspace that I have zero tap gesture recognizers myself. Crucially, the issue affects the third tab too, which never uses tap gesture recognizers.
I am going slightly insane looking at this, so anyone that can clarify why tabs might be switching despite very clearly not tapping another tab would be hugely appreciated.
For specs, I am running on a MacBook Pro (macOS Catalina), with Xcode 12.4 and running iOS 14.1 on an iPhone SE 2nd Gen and iPhone 12 (both simulated) and an iPhone 7 (real).

Related

ViewController in navigation stack won't deinit when popped, causing memory leak

In my app I have a RootViewController which all my ViewControllers are subclassed from. When developing, I usually use this in my RootVC:
deinit {
print("\(type(of: self)) deinit")
}
so that I always can see when any of my viewControllers deinit. It prints:
MyExampleViewController deinit
Today I noticed that one of them didn't deinit when I navigated away from it. Let's call it DetailViewController. It's a completely normal (Root)ViewController-subclass pushed into the main NavigationController. When hitting the Back button in the navigation, it navigates away, but never says it deinits. This is the first pushed controller, so I can't pop the controller before to see if that helps. But any controller pushed after the DetailViewController gets deinited fine when navigating back and forth.
I decided to check the memory graph, so I ran my app again, pushed to the DetailViewController, then popped it away by clicking the Back button in the navigation, then I clicked Debug memory graph.
In the debug navigator on the left, I scroll down and see that there exists one instance of my DetailViewController. If I push back and forth several times before opening the memory graph, there are as many different instances of this DetailViewController as times I've pushed and popped.
When clicking it, I see this:
The DetailViewController is the single controller on the far right. I haven't used Memory Graph that much, but I assume that the "solid" white lines are strong claims, and that the slightly more transparent (gray) lines are weak claims. Meaning that there's one strong claim to my controller. The one on the bottom.
This is the bottom row:
What does this mean? It seems like my (custom) NavigationController has an array called _childViewControllers which retains my popped controller. To clarify, I don't have any stored variables in my custom NavigationController. It's only subclassed to override 5 functions, that is all. I have about 20 different ViewControllers being pushed and popped by this exact same custom `NavigationController, but they all have no problem with this.
Am I reading the graph wrong? There has to be a different strong claim that's not visible in the graph, right? When I "pop" the viewController by clicking Back, shouldn't my viewController be removed by _childViewControllers?
Figured it out at last. Unfortunately, I had to go through commenting out several hundred lines of code bit by bit until I found out when it started to deinit when expected.
The issue was a missing [weak self] in a closure, not unexpected, but it was in a completely different class, connected through a complicated hierarchy.

after push UIViewController app freeze swift

I use UITabBarController as the root controller, each TabBarItem has its own UINavigationController, the first UITabBar (0) refers to the UIViewController which displays the results from the server. The fifth UITabBar (4) includes the MoreFormViewController (it is the successor of the FormViewController from the Eureka framework) it has several cells (two are currently implemented). Clicking on one of them opens SettingsFormViewController (this is the successor from FormViewController from the Eureka framework), the first cell contains UISlider, which changes the settings for getting data from the server (which are displayed in UITabBar (0)). If this parameter changes, then when we return back to the MoreFormViewController, we automatically make the transition to UITabBar (0). If you quickly switch from UITabBar (0) again to the MoreFormViewController and open the SettingsFormViewController, the application is frozen. Found out the following that at this point SettingsFormViewController is initialized and all life cycle methods are triggered (viewDidLoad, viewWillAppear, viewDidAppear and so on). But the screen is not visible, then I looked at UI View hierarchical in Xcode, and it shows that the very first controller is SettingsFormViewController as it should be. All this time I checked on a real iPhone 7, then I decided to check on the simulator and iPhone 5, they did not find such a bug on it, everything works correctly. During the next test, when the application "froze" on the iPhone 7, I accidentally folded it and unfolded it and I saw this screen and it completely works. But if you repeat the same action, then again the application "freezes."
I was looking for information on this, but at the most I found that people simply "frozen" the application because they were pushing the controller out of the main thread. So I do not know what to do. Can you tell me how to solve this problem?
Also I tried to replace the SettingsFormViewController with a regular UIViewController with a slider, the result is the same. Removed everywhere isUserInteractionEnabled = false even in third-party frameworks to know exactly what is not the problem.

UISplitViewController: Deinit DetailView in collapsed mode

I've been struggling on this for a while now, but I wasn't able to find a solution:
I've got an iOS 9 app that supports all device families, uses size classes and is programmed with Swift 2.0.
I'm using a UISplitViewController and everything works as I want, except in a collapsed environment (e.g. on an iPhone).
The Master-ViewController is a UITableViewController that triggers a replace segue when a cell is selected. In a collapsed environment this means, that the detailViewcontroller gets pushed onto the screen. The UISplitViewController visually behaves kind of like a UINavigationController.
However, when I dismiss the detailViewController with the back button or the swipe gesture it does not get deallocated until the a new replace segue is triggered in the Master-ViewController.
I assume that this is kind of a feature of UISplitViewController, since it was originally designed to show both contents next to each other. Nevertheless, in a collapsed environment I would like my UISplitViewController to behave like a simple UINavigationController, which deallocates the previously pushed detailviewController when popped.
I've been trying to manually change the splitViewController's viewControllers attribute after the detailViewController is popped:
if let firstVc = self.splitViewController?.viewControllers.first {
self.splitViewController?.viewControllers = [firstVc]
}
But that does not help. Simply replacing the detailViewController with an empty "Dummy"-ViewController doesn't work neither, since it automatically animates the transition.
Playing around with the UISplitViewControllerDelegate didn't help me neither...
Is there a solution for this (maybe simple? :)), that I'm too blind to see?

Switching between UIViewControllers in story board

As someone who usually used separate xibs in the past I thought I'd give storyboard a go as it seemed a lot simpler to use and much easier to develop with. I've been writing an application where the essential set up is this:
At the top of all this is a UINavigationController (first level). Then I have Multiple UIViewControllers (second level) with buttons in them which you can tap to switch between the second level UIViewControllers.
However a problem occurs when I start switching between the second level UIViewControllers. I first thought this was an initialisation problem with the NSMutableArrays because in my code I have a NSTimer set to loop periodically and found when I set a breakpoint during it, when I went forward to the next timer tick event there appeared to be different instances of the same NSMutableArrays and it seemed a gamble to try and insert new values into these array with it sometimes working, sometimes not (as it may or may not insert into the correct instance).
Then, looking at the memory usage under Debug Navigator I found the issue. Each time I "switched" between the UIViewControllers a new UIViewController was being initiated, along with all new variables.
The code I am using to switch between them is
-(void) perform {
[[[self sourceViewController] navigationController] pushViewController:[self destinationViewController] animated:NO];
}
Or essentially a push segue transition. This also explains why when I tried to switch back to my view, the data on that view was lost as it is a complete new view.
Does anyone know how to switch between multiple ones of these UIViewControllers (say 5) essentially like a UITabViewController would except without the tab bar being present?
First option you can do this: You can use a tabbarcontroller for switching viewcontroller and hidden the tabbar. Then on buttonclick setthe tabbar index.
Second option you can do this: Create one more view controller and in this viewcontroller subview the all switching viewController and when you want to switch viewcontroller just bring that viewcontroller view to front by delegate.
Do you need the navigation bar and other features provided by your top level navigation controller?
If not, you could use a UIPageViewController instead.
You set up all your second level view controllers and then just have to tell the page view controller which one to display.
If you implement the associated delegate methods, it will automatically provide swipe gestures to switch between them and nice animations to get them on and off screen.
You can also get it to put a UIPageControl at the bottom showing a dot for each VC with the dot for the current VC highlighted.

Memory management of view while using NavigationController

I changed navigation in my application from using UITabBarController to u UINavigationController. I.e. former solution (1st version) was based only on the TabBarController - 4 ViewControllers (one simple TableView, one simple custom view and one MapView with many overlays). The second version is based only on the UINavigationController.
In case of TabBarController it was clear and simple, everything worked fine, especially MapView. I mean: the MapView was loaded once (with a significant number of overlays) and when I went to another view and back to the MapView the MapView was still there with its overlays already loaded and displayed (simple check: MapView`s viewDidLoad was called just once per app run, I had some debug messages there).
Now I changed navigation logic to the UINavigationController. Everything works fine for the first look - but: the viewDidLoad (for each view) is called everytime I navigate to the view. It is annoying especially in the case of the MapView - the loading of overlays is performed everytime, it takes some time and it causes app crash in some cases.
OK, my questions:
Is it some kind of "common" behavior of NavigationController?
Can I change this behavior so viewDidLoad will be called just once?
And more - How can I influence the "display sequence" of some view?
I understand the logic is probably more complicated but I appreciate any answer or hint ;)
Some related circumstances:
TabBar and Navigation controllers are not combined.
I use storyboards, segues are designed in the UIB, no manual calling like perfomSegue or prepareForSegue in my code. One button triggers segue to MapView.
I use push segues.
I also tried to use modal segues but without any change of that behavior.
any of viewDidUnload is never called during segues among the views.
No memory warning received.
No memory leaks measured both on simulator and iPhone 4.
I tried to build a very simple temporary project / app that is concerned just about the Nav. Controller and other views without ANY coding, just storyboard. It was the same behavior.
There was an issue that causes app crash when I fast and periodically tapped to navigation button and back button between one view and the MapView. In most cases the app crashed when I tapped the back button on the MapView before it was fully displayed (i.e. its overlays). It was fixed when I added a 1 sec. delay method call in the viewDidDisappeared in the MapView. It is not a fair fix, I know ;)
A UITabBarController and UINavigationController are based on fundamentally different paradigms.
The UITabBarController is intended for the UIViewController on each tab to exist independently of each other and for the user to choose which they want to view. viewDidLoad only gets called once for each UIViewController because it is intended that each tab still exists in memory even as the user switches to a different tab.
The UINavigationController is a stack of UIViewControllers where each is related to the one above and beneath itself. The top UIViewController in the stack is always the one that is visible to the user. When a UIViewController is pushed to the stack, its viewDidLoad gets called because it is being loaded into memory. When the top UIViewControllergets poped off the stack, it is unloaded from memory, and viewDidUnload gets called on the way out (viewDidUnload is deprecated in iOS6 and won't get called, but the controller will still get dumped from memory). This is why viewDidLoad gets called every time that the user pushes a particular UIViewController onto the UINavigationController stack.

Resources