iOS - presentViewControllerAnimated vs window.rootViewController - ios

I have a class MasterViewController that loads on app startup. Inside viewDidLoad() it checks if a user is logged in, and presents one view controller or another based on the outcome. If you aren't logged in and then proceed to do so, the app loads up a new MasterViewController. My goal is to essentially replace the existing MasterViewController with a new instance so it performs the check in viewDidLoad once again. I've tried the following, and they both work:
// changing the root view controller
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.window = UIWindow(frame: UIScreen.mainScreen().bounds)
appDelegate.window!.rootViewController = MasterViewController()
appDelegate.window!.makeKeyAndVisible()
// using presentViewControllerAnimated
someViewController.presentViewControllerAnimated(MasterViewController(), animated: true, completion: nil)
...but while presentViewControllerAnimated has a nice animation, changing root doesn't. More importantly, changing the root view controller doesn't destroy the existing one (at least deinit is never called..), and obviously presentViewControllerAnimated doesn't do that either, so in both cases I have this view controller floating around that I don't want anymore.
I can just imagine some scenario where a user logs out and back in repeatedly and suddenly I have 10 MasterViewControllers on top of one another. Any way to completely purge a view controller? Or is this just totally unnecessary?
EDIT
Just remembered presentViewControllerAnimated is for presenting a vc modally, so that's definitely not what I want. Would be nice to change the root view controller with a similar animation though. All the animations I've seen with root vc changes were pretty wonky.

The following Stackoverflow thread touches on your concern about the old UIViewController sticking around in memory:
Stackoverflow: Changing root view controller of a iOS Window
serge-k's answer comes from the UIWindow Class Reference:
var rootViewController: UIViewController?
The root view controller for the window.
The root view controller provides the content view of the window.
Assigning a view controller to this property (either programmatically
or using Interface Builder) installs the view controller’s view as the
content view of the window. If the window has an existing view
hierarchy, the old views are removed before the new ones are
installed. The default value of this property is nil.
The Resource Management in UIViewController section of the UIViewController Programming Guide also contains a note on this:
The default behavior for a view controller is to load its view hierarchy
when the view property is first accessed and thereafter keep it in memory
until the view controller is disposed of. The memory used by a view to
draw itself onscreen is potentially quite large. However, the system
automatically releases these expensive resources when the view is not
attached to a window. The remaining memory used by most views is small
enough that it is not worth it for the system to automatically purge and
recreate the view hierarchy.
You can explicitly release the view hierarchy if that additional memory
is necessary for your app. Listing 4-3 overrides the
didReceiveMemoryWarning method to accomplish this. First, is calls the
superclass’s implementation to get any required default behavior. Then,
it cleans up the view controller’s resources. Finally, it tests to see if
the view controller’s view is not onscreen. If the view is associated
with a window, then it cleans up any of the view controller’s strong
references to the view and its subviews. If the views stored data that
needs to be recreated, the implementation of this method should save that
data before releasing any of the references to those views.

Following code and concept is helpful for you
var window: UIWindow?
var viewController: MasterViewController?
var navigationVC:UINavigationController?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool
{
window = UIWindow(frame: UIScreen.mainScreen().bounds)
if let window = window {
window.backgroundColor = UIColor.whiteColor()
viewController = MasterViewController(nibName: "MasterViewController", bundle: nil);
//If you want NavigtionController
navigationVC = UINavigationController(rootViewController: viewController!)
navigationVC?.navigationBar.hidden = true
window.rootViewController = navigationVC
//Or If you don't want navigation controller
window.rootViewController = viewController
window.makeKeyAndVisible()
}
return true
}
Here
1.The root view controller provides the content view of the window. Assigning a view controller to this property (either programmatically or using Interface Builder) installs the view controller’s view as the content view of the window.
2.If the window has an existing view hierarchy, the old views are removed before the new ones are installed.
3.The default value of this property is nil.

Related

Do UIViewControllers get destroyed and recreated in iOS during lifecycle events? Or do they simply get "re-attached"?

Let us consider the following example subclass of a UIViewController
import Foundation
class CustomViewController: UIViewController
{
override func viewDidLoad()
{
super.viewDidLoad()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
}
init()
{
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder)
{
super.init(coder: coder)
}
}
According to the following stackoverflow post
viewDidLoad(...) will be called once whenever the view controller needs to load its view hierarchy. Obviously, that'll happen the first time that the controller accesses its view. If the view controller later unloads its view, then viewDidLoad(...) will be called again the next time the view is loaded. A view controller won't unload its view just because the view is hidden, but it might do so if memory starts to run low.
But what about the UIViewController itself as an object in memory? During low memory events, or when the app has been placed into the background, will the UIViewController object simply be discarded and recreated with init(...)? Or will it be guaranteed a lifeline and be preserved - so only viewDidLoad(...) will be called again?
The entire premise of your question is incorrect.
The question and answer you cite are way outdated and wrong. Views are not magically unloaded and reloaded. viewDidLoad can never be called more than once by the runtime on a given VC instance, and using it for onetime early lifetime tasks is correct.
As for your question itself: No VC will ever be forcibly destroyed by the runtime due to low memory. If your memory usage is excessive and you fail to reduce it, the runtime does not dismantle piecemeal the object structure of your app; it kills your entire app.
Adding to Matt's answer:
View controllers get created when it is time to display them, and persist until UIKit is done with them.
The lifecycle of a view controller depends on how you manage it. Often that's based on what kind of parent view controller hosts it.
Some examples (not an exhaustive list)
If a view controller is displayed modally, it is likely created, displayed, and then discarded once it is dismissed.
Navigation controllers manage a "stack" of view controllers. All the view controllers in the stack exist in memory for the whole time they are on the stack. You create a new view controller and push it onto the stack to display it. If you pop one or more view controllers off the stack, the navigation controller releases its strong reference to them, and they will be deallocated unless you keep another strong reference to them somewhere (which is not typical.)
You created all the view controllers that will be displayed in a tab bar controller at the time you create the tab bar controller, and they persist for the life of the tab bar controller.
Page view controllers keep strong references to the page(s) being displayed, and possibly to the next and previous view controller. Other page's view controllers get discarded, and new view controllers are created to display new pages.
As Matt pointed out in his answer, the quote you included about viewDidLoad is out of date and wrong. (I think that was from iOS 3, if memory serves.) A view controller's viewDidLoad method is called once and only once in its lifetime, when it's views are first loaded. The view controller's views will never be deallocated until the view controller itself is deallocated.

What is the difference between presenting a ViewController vs adding a ViewController as a child

I just read an interesting article that introduced me to a new concept that I have never heard before, which is adding a ViewController as a child of another ViewController. The article uses the UIActivityIndicatorView as an example to demonstrate a common use for when to use the addChild method and this got me wondering why would you use that method instead of just presenting the view controller.
The part I don't fully understand is the benefit of using that specific method since both presenting and adding as a child could be modular in a way that they can be reusable.
When would you use addChild instead of just presenting it? When does that make sense?
addChild:
let parent = UIViewController()
let child = MyViewController()
parent.view.addSubview(child.view)
parent.addChild(child)
child.didMove(toParent: parent)
Present:
let storyboard : UIStoryboard = UIStoryboard(name: "StoryboardName", bundle:nil)
let myVC = storyboard.instantiateViewController(withIdentifier: "myViewController") as! MyViewController
self.present(myVC , animated: true, completion: nil)
The difference is that when you present a controller, it becomes modal, and when you add it as a child controller, it doesn't. The difference is in semantics and use cases.
Modal
As an example of modal window, imagine a window with an OK button in macOS that prevents you from interacting with the parent window until you close it.
Hence why there can be only one view controller presented by a particular controller: the presented controller becomes “main” for user interaction, and the user must close it to return to the parent window.
A typical example of a modal window is an alert.
Child
But a child view controller is a different story. There can be any amount of them on a particular VC.
The parent controller becomes sort of container for child view controllers. For example, there can be a dashboard with multiple panels, and each panel can be a separate controller. This allows to separate logic between different pieces of an application instead of cramming everything into a single controller, to implement user customization in easy way, etc.
A typical example of a parent-child controller interaction is UINavigationController: the navigation controller is the parent, and all view controllers pushed into it are its children. We don't make an uber controller with the whole application's logic, we separate it between different screens, and the navigation controller is only responsible for presentation.
It's very simple. addChild is when you want to embed the view controller in you parent view controller (you will also need to follow up with a view.addSubview(child.view)). present is when you want to, well, present it... think of it as a change of screens.
addChild:
present:

ViewController Modal Presentation not appearing

I have a view controller that is a child view controller of my window's root view controller. That child view controller has a table view and when i select a row it tells the parent view controller to present a view controller modally. The modal view controller, however, never appears. I created a bare bones test view controller that just prints viewDidLoad and viewWillAppear. What I notice is that when I call parentVC.present(testVC, animated:true, completion:nil), viewDidLoad is run, but viewWillAppear is not. viewWillAppear is only then called when I interact with the UI in some way. Whether tapping, panning, scrolling or whatever.
I've spent hours trying to debug this. It doesn't seem like the main queue is blocked and I've reduced the problem to its bare bones. The modally presented view controller's viewWillAppear is simply not called until I interact with the UI again.
What could be causing this symptom?
In comments, you mention that you're instantiating your view controller with
let vc = TestVC()
where TestVC is presumably a (largely empty) UIViewController subclass.
A view controller needs a view created either via either storyboard scene (using instantiateViewController), a NIB or, in very rare cases, a view you create in loadView (which you shouldn’t be confused with viewDidLoad).
I’d suggest creating a storyboard scene (assuming you are using storyboards), give it a storyboard ID, and then use instantiateViewController:
let vc = storyboard.instantiateViewController(withIdentifier: "foo")
But just having a UIViewController subclass called TestVC and instantiating it with TestVC() won’t work.
In our discussion, you said you wanted to do this programmatically with no NIB nor storyboard. If so, use loadView. See https://stackoverflow.com/a/37964249/1271826 for an example.

How to set initial storyboard

I have two storyboards one of them is default called Main and the other one I have just added is called Admin.
Main is used for customer, Admin is used for owner of the app.
I wonder how do you set initial/main interface storyboard programmatically.
P.S. I know how to change it via xcode but do not know programatically.
You do not set an initial storyboard programmatically.
Here's how it works. Either you have a main storyboard listed in Xcode under Main Interface or you don't:
If you do, that is the initial storyboard, period. That storyboard is loaded automatically at launch time, and its initial view controller becomes the window's root view controller (and the interface is then displayed automatically).
If you don't (that is, if the Main Interface field is empty), then nothing happens automatically. It is up to your code to obtain a view controller, somehow (possibly from a storyboard), and make its the window's root view controller (and to show the interface).
So, to sum up, either everything happens automatically or nothing happens automatically. There is no intermediate state, as you seem to imagine, in which you can programmatically change things so that a different storyboard is loaded automatically.
There is, however, a kind of intermediate state where you permit the Main Interface storyboard to load but then you ignore it. In your implementation of application:didFinishLoading..., you would then sometimes do the thing I said in the second bullet point, i.e. load a different view controller and make it the root view controller. This works because the automatic stuff I mentioned in the first bullet point has already happened by the time application:didFinishLoading... is called. So, in effect, your code sometimes permits that automatic stuff and sometimes overrides it.
In this example from my own code, we either use the Main Interface storyboard to load the initial view controller or we load a different view controller from the storyboard ourselves, depending on a value in User Defaults:
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
if let rvc = self.window?.rootViewController {
if NSUserDefaults.standardUserDefaults().objectForKey("username") as? String != nil {
self.window!.rootViewController = rvc.storyboard!.instantiateViewControllerWithIdentifier("root")
}
}
return true
}
You can set your storyboard programmatically like this in the app delegate:
window = UIWindow(frame: UIScreen.main.bounds)
// Or "Admin"
window!.rootViewController = UIStoryboard(name: "Main", bundle: nil).instantiateInitialViewController()!
window!.makeKeyAndVisible()
You could actually set any view controller. That's how no-storyboard approach is wired up.
Oh, another advantage of doing it in code is it delays the storyboard initialization. If you use the "Main Interface" settings in Xcode, the storyboard is actually initialized BEFORE the application:didFinishLaunchingWithOptions method.

How to properly handle a nil UIApplication.sharedApplication().keyWindow

When the viewDidLoad is called the view is supposed to be loaded.
But I always crash in UIApplication.sharedApplication().keyWindow being nil...
Where should I put my code to have it called once the view is loaded and not everytime the user comes back (I have excluded viewWillAppear for that reason) ?
Situation 1: - Manual UIWindow creation in App's delegate
You have probably somehow added a UIViewController to a UIWindow before setting it as key.
You should call window.makeKeyAndVisible() in your app's delegate after creating the window.
Situation 2: - Automatic storyboard instantiation
The system reads your storyboard, initializes the root view controller, prepares it by calling viewDidLoad and viewWillAppear, adds it to the window and shows the window.
What happens is the system cannot just set the window to the front and animate the view controller onto it, because it's the first view controller and you are not push/popping of a nav controller. And viewDidLoad can take some time... So the Default.png is showing in the meanwhile until the view controller is ready.
Then it calls viewDidAppear, when it has actually appeared. So that's where you should be able to access a keyWindow
Situation 3:
You are in a weird situation where you have windows but none of them is the "key" window currently, but you desperately need to access the window.
Call UIApplication.sharedApplication().windows, see if it's not empty and take the first element. UIApplication.sharedApplication().delegate?.window might have a value too, but it's usually only when the window is key already.
Try using the window property of the application delegate:
UIApplication.sharedApplication().delegate!.window
If you get nil from "UIApplication.shared.keyWindow" at "viewDidLoad", Try wrap your code inside of DispatchQueue.
ex)
override func viewDidLoad() {
DispatchQueue.main.async {
if var window = UIApplication.shared.keyWindow {
// do something
}
}
}

Resources