I have a modal view on top of a navigation controller.
I wish to destroy the whole stack and re-create a new one (reload).
However, when assigning the new one to window.rootViewController, warnings appears during runtime and view controllers are not deallocated.
To make things more complicated, I am auto navigating to the modal view controller 'automatically' upon reload, and that < iOS 12 and iOS 13 behaves differently.
I have attached a reprex that demonstrates the issues that will appear. E.g. if you run it as is, on iOS 13, you will see the counter jump from 1 to 3, while on iOS 12, it jumps from 1 to 2. Both are leaking memory (some or all view controllers are not being unloaded).
The main issue is that regardless of how you replace the view hierarchy, your presented VC will dismiss which will send a viewWillAppear message to your "pushedVC" ... at which point "pushedVC" will immediately load and re-present "triggerVC"
What you probably want to do is:
Leave jumpToModalVC equal to false, until you want to use it
On button tap in presented "triggerVC", dismiss the modal (self)
on completion of the dismiss, set jumpToModalVC to true and rebuild / reset your hierarchy
Note:
Chained calls to segues from each VC's viewWillAppear almost always leads to:
Unbalanced calls to begin/end appearance transitions
To avoid that, it's best to trigger the segues from viewDidAppear
If you want, you can add me as a "Collaborator" on your GitHub repo (my GitHub user ID is DonMag), and I can push the changes I made as a new branch.
Related
I have a strange problem with state restoration for a Universal app with Split View Controller.
The strange thing that I am doing things in a very standard way using a Storyboard and segues and with a restoration identifier for alle relevant view controllers. There is not really any code, as the logic is in the Storyboard and a minimal XCode project shows this.
The problem is with a settings screen that is shown modally as a form sheet presented from the split view controller. My view controller hierarchy ends up correct, but the transition doesn't really make sense. For some reason state restoration animates the modal controller into place.
Since the screen starts out with a screenshot from the last time the app was running, with the settings controller already present, the animation is just visual noise.
I have tried to disable animation on the segue which is respected when entering the settings interactively, but when state restoration does the same thing, the animation is there.
What is the standard way to avoid this?
Calling self.window?.makeKeyAndVisible() in application(_:willFinishLaunchingWithOptions:) solved the issue for me.
More info in the docs:
Important
If your app relies on the state restoration machinery to restore its
view controllers, always show your app’s window from this method. Do
not show the window in your app’s
application:didFinishLaunchingWithOptions: method. Calling the
window’s makeKeyAndVisible method does not make the window visible
right away anyway. UIKit waits until your app’s
application:didFinishLaunchingWithOptions: method finishes before
making the window visible on the screen.
Given the following view controller layout.
We build a stack of modal view controllers by first presenting B on A and then presenting C on B. According to the Apple documentation on dismiss(animated:completion:), calling it on A should actually dismiss the topmost view controller (C in this case) in an animated fashion and all intermediate view controllers without animation. What happens though is that C gets dismissed without animation and B is dismissed in an animated fashion.
I put up an Xcode project on GitHub that replicates that behaviour. Am I missing something or am I misunderstanding the documentation here?
After poking around the web and trying out various 'solutions' it is clear this is an actual bug within iOS. It has been present since iOS 8... and is still present in iOS 10. It was originally reported in iOS 8, but the solution was never validated and Apple automatically closed the radar due to inactivity.
I have filed a new radar as this is in direct contradiction to the documentation for dismissViewController
If you present several view controllers in succession, thus building a
stack of presented view controllers, calling this method(means
-[UIViewController dismissViewControllerAnimated:completion]) on a view controller lower in the stack dismisses its immediate child view
controller and all view controllers above that child on the stack.
When this happens, only the top-most view is dismissed in an animated
fashion; any intermediate view controllers are simply removed from the
stack.
Clear visualization of the issue, both expected and actual results. Credit to Boris Survorov for the test project and visualizations.
I've experienced the same issue and here is what I've found to be a viable workaround. When you need to dismiss the whole stack, execute this code in A:
viewControllerB.view.isHidden = true
viewControllerC.dismiss(animated: true) // or viewControllerB.dismiss(animated:true) - it should produce the same result: dismiss viewControllerC
dismiss(animated: false) // dismisses viewControllerB
This should result with the expected behavior.
I am guessing that your segue from A to B is modal as well? In that case the dismiss function called from A wants to dismiss the view, which is immediately on top of A, which is B. C just gets hidden in order to show you the animated hiding of B. In that sense you cannot stack views via modal segues and dismiss the top one with the dismiss function as you described if you go that far back. The dismiss would work as intended if called from B to dismiss C though.
I got some serious problems understanding the viewcontroller stack.
When will my app use a stack to save the previous viewcontrollers? Only if I use a navigation viewcontroller or anytime I use normal viewcontrollers and segue modally between them?
So I was just wondering if I use some sort of chained routine for example, like going from vc 1 to vc 2 and from vc 2 back to vc 1. No navigation controller, just modal segues, no unwinding.
Does my app got performance issues because of a stack (which will grow everytime I go around) or doesn't it make any difference?
----updated
So basicly this is my problem. If I went through the routine of the app, the views get stacked everytime I do a transtition.
UINavigationController will retain any controller you push onto it's navigation stack until you pop it back off.
Any UIViewController will retain a controller it presents modally until that child controller is dismissed.
In either case every controller will at a minimum consume some memory until you remove it. Apps which construct ever expanding stacks of controllers are likely to encounter a number of issues including:
you will eventually run out of memory, how fast depends on how much memory each controller uses.
you may see unexpected side effects if many controllers in the background react to the same event.
users may become confused if they change state in an instance of controller 'A', push an instance of controller 'B' on top of it, and then "return" to a second instance of 'A' added to the top of the state. Since they're looking at a new controller and view whatever selection, scroll position, user input, or other state they set on the previous instance may be lost.
developers, including you, may come to dread touching this app.
I suspect that everyone will have a better experience if you view controller management matches whatever visual metaphor you are presenting to the user.
Summary of question
UINavigationControllerDelegate:didShowViewContoller makes it possible to get notified whenever any view controller has been displayed (as opposed to being loaded), provided its within the context of a navigation stack.
I want to know if such observation is possible for all view controllers if there isn't a navigation stack.
More background
I have an app where view controllers can suddenly appear based upon timers and local notifications firing, thus their appearance is effectively random.
If one VC triggers and gets displayed at the same time as another was in the process of getting displayed then there can be an issue (if you're experienced with iOS you'll be aware if one VC pushes another from within its viewDidLoad, rather than its viewDidAppear you will get an "attempting to present X on Y whose view is not in the window hierarchy" error)
How I solve this is I have a list of VCs to display and they get displayed by a view controller co-ordinator which implements UINavigationControllerDelegate's didShowViewContoller and doesn't display the new VC until didShowViewController has been invoked.
This works perfectly.
But now my problem is I want to do a similar thing for an app that isn't using a navigation controller, and thus I can't use UINavigationControllerDelegate:didShowViewController to observe globally when a view controller has been displayed. Does anybody know of another elegant mechanism for doing so?
I'm experiencing something really weird :
Create an extremely basic single view project, and add a second view controller to the storyboard, along with a modal segue from the first to the second. Initiate the segue from the view controller and trigger it programmatically with performSegueWithIdentifier:.
In the viewDidLoad of the modally presented view controller, add this log :
NSLog(#"%#", self.presentingViewController);
Now run the app on iOS 7, you should get a log like this one :
<ViewController: 0x7fa8e9530080>
Which is just the reference of the initial view controller of the app, which presented the modal view controller.
Now run the exact same thing on iOS 8, and you will get :
(null)
What's going on here ? Is it a known issue ? Of course I'd expect the exact same behavior on both systems.
Thanks ... Formalizsed as answer.
viewDidLoad should really be used for initialization, At this stage, there is not guarantee that the receiver's controllers view hierarchy has been placed in the navigation tree. If that is your intent, you should override viewWillAppear or viewDidAppear. Whilst it works in earlier versions, the docs clearly state that it should be used for additional initialization. It certainly sounds as though in iOS 8, the receiver's initialization is being performed earlier