In my app I programmatically change root view controllers based on user actions e.g. login/logout functionality.
In iOS 8 - I'm noticing a strange issue. Even after setting rootViewController on the window, the old hierarchy still persists. I just verified it by capturing view hierarchy.
- (void) logout{
[self.window setRootViewController:[self loadLoginView]];
}
-(UIViewController *) loadLoginView{
WelcomeScreenVC *wsVC;
wsVC = [[WelcomeScreenVC alloc] initWithNibName:#"WelcomeScreenVC" bundle:nil];
UINavigationController *onboardingVC = [[UINavigationController alloc]initWithRootViewController:wsVC];
return onboardingVC;
}
Even after executing this line of code, the old logged in view hierarchy still persists. Would appreciate if anybody can suggest what's happening behind the scenes.
Edit: I just looked at UIWindow setRootViewController documentation and here's what Apple has to say about it:
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.
I have noticed the very same thing.
Basically, I have a fairly complicated storyboard that acts as a login/welcome interface. This interface sits in a navigation controller, which presents another navigation controller modally.
After a certain point, the user takes an action that transitions him to the main interface. Using the iOS 8 view debugger I noticed that the old view hierarchy was still around after setting the rootViewController property of the window.
My solution, for now is to use the following code, right before I re-assing the window.rootViewController property:
for (UIView* subView in self.window.rootViewController.view.subviews) {
[subView removeFromSuperview];
}
[self.window.rootViewController.view removeFromSuperview];
It ain't pretty, but it works.
Another odd thing I noticed is that the welcome interface's modally presented viewController is not properly cleaned up using this method. I have to manually dismiss it AND do this clean up.
The best way to fix is:
self.window.subviews.forEach { $0.removeFromSuperview() }
or, in old style:
for view in self.window.subviews {
view.removeFromSuperview()
}
var loginNavigationController: OnBoardViewController?{
willSet{
if newValue == nil {
loginNavigationController?.view.removeFromSuperview()
}
}
}
loginNavigationController = nil
Then set window.rootviewcontroller = {Different VC}
Related
I'm experiencing a memory leak (the UINavigationController and its root View Controller are both being leaked) when presenting and dismissing a UINavigationController in a subview. My method of presentation of the navigation controller seems a bit non-standard, so I was hoping someone in the SO community might be able to help.
1. Presentation
The Navigation Controller is presented as follows:
-(void) presentSubNavigationControllerWithRootViewControllerIdentifier:(NSString *)rootViewControllerIdentifier inStoryboardWithName:(NSString *)storyboardName {
// grab the root view controller from a storyboard
UIStoryboard * storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil];
UIViewController * rootViewController = [storyboard instantiateViewControllerWithIdentifier:rootViewControllerIdentifier];
// instantiate the navigation controller
UINavigationController * nc = [[UINavigationController alloc] initWithRootViewController:rootViewController];
// perform some layout configuration that should be inconsequential to memory management (right?)
[nc setNavigationBarHidden:YES];
[nc setEdgesForExtendedLayout:UIRectEdgeLeft | UIRectEdgeRight | UIRectEdgeBottom];
nc.view.frame = _navControllerParentView.bounds;
// install the navigation controller (_navControllerParentView is a persisted IBOutlet)
[_navControllerParentView addSubview:nc.view];
// strong reference for easy access
[self setSubNavigationController:nc];
}
At this point, my expectation is that the only "owner" of the navigation controller is the parent view controller (in this case, self). However, when dismissing the navigation controller as shown below, it is not deallocated (and as a result its rootViewController is also leaked, and so on down the ownership tree).
2. Dismissal
Dismissal is pretty simple, but it seems not to be sufficient for proper memory management:
-(void) dismissSubNavigationController {
// prevent an orphan view from remaining in the view hierarchy
[_subNavigationController.view removeFromSuperview];
// release our reference to the navigation controller
[self setSubNavigationController:nil];
}
Surely something else is "retaining" the navigation controller as it is not deallocated. I don't think it could possibly be the root view controller retaining it, could it?
Some research has suggested that retainCount is meaningless, but FWIW I've determined that it remains at 2 after dismissal, where I would expect it to be zero.
Is there an entirely different / better method of presenting the subNavigationController? Maybe defining the navigation controller in the storyboard would have greater benefit than simply eliminating the need for a few lines of code?
It is best practice when adding a controller's view as a subview of another controller's view, that you make that added view's controller a child view controller; that is, the controller whose view your adding it to, should implement the custom container controller api. An easy way to set this up is to use a container view in the storyboard which gives you an embedded controller automatically (you can select that controller and, in the edit menu, choose embed in Navigation controller to get the UI you're trying to make). Normally, this embedded view controller would be added right after the parent controller's view is loaded, but you can suppress that by implementing shouldPerformSegueWithIdentifier:sender:. I created a simple test app with this storyboard,
The code in ViewController to suppress the initial presentation, and the button methods to subsequently present and dismiss it is below,
#implementation ViewController
-(BOOL)shouldPerformSegueWithIdentifier:(NSString *)identifier sender:(id)sender {
if ([identifier isEqualToString:#"Embed"]) { // The embed segue in IB was given this identifier. This method is not called when calling performSegueWithIdentifier:sender: in code (as in the button method below)
return NO;
}else{
return YES;
}
}
- (IBAction)showEmbed:(UIButton *)sender {
[self performSegueWithIdentifier:#"Embed" sender:self];
}
- (IBAction)dismissEmbed:(UIButton *)sender {
[[self.childViewControllers.firstObject view] removeFromSuperview];
[self.childViewControllers.firstObject willMoveToParentViewController:nil];
[self.childViewControllers.firstObject removeFromParentViewController];
}
#end
The navigation controller and any of its child view controllers are properly deallocated when the Dismiss button is touched.
The navigationController property on a UIViewController is retain/strong, which is presumably the other strong reference.
So try popping all view controllers from the navigation controller and see if that works.
My application has a setup screen that should be presented modally on the root view controller if certain conditions are met.
I have looked around on SO and the internet and the closest answer so far to how to go about doing this is here:
AppDelegate, rootViewController and presentViewController
There are 2 problems with this approach however:
In iOS 8, doing it this way makes a log appear in the console, which doesn't seem to be an error, but is probably not good nonetheless:
Unbalanced calls to begin/end appearance transitions for UITabBarController: 0x7fe20058d570.
The root view controller actually shows up very briefly when the app launches, and then fades into the presented view controller (even though I explicitly call animated:NO on my presentViewController method).
I understand that I can set my root controller dynamically in applicationDidFinishLaunchingWithOptions: but I specifically want to present the setup screen modally, so that when the user is done with it, it dismisses and the true first view of the application is revealed. This is to say, I don't want to dynamically change my root view controller to my setup screen, and present my app experience modally when the user is done setting up.
Presenting the view controller on my root view controller viewDidLoad method also leads to a noticeable blink of the UI when the app is launched for the first time.
Is it possible to programmatically present a view controller modally, before the application has rendered anything so that the first view in place is the modal view controller?
UPDATE: Thank you for the comments, adding my current code as suggested:
In my AppDelegate.m:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"Main" bundle:nil];
[self.window makeKeyAndVisible];
[self.window.rootViewController presentViewController:[storyboard instantiateViewControllerWithIdentifier:#"setupViewController"] animated:NO completion:NULL];
return YES;
}
This does what I need except for the fact that it briefly shows the window's root view controller for a second when the application launches, then fades the setupViewController, which I find odd given that I am presenting it without animation and fading is not how a modal view controller is presented anyway.
The only thing that has gotten me close is manually adding the view in the root view controller's view did load method like so:
- (void)viewDidLoad
{
[self.view addSubview:setupViewController.view];
[self addChildViewController:setupViewController];
}
The problem with this approach is that I can no longer "natively" dismiss the setupViewController, and will now need to deal with the view hierarchy and animated it out myself, which is fine if it's the only solution, but I was hoping there was a sanctioned way of adding a view controller modally without animation before the root view controller displays.
UPDATE 2: After trying a lot of things out and waiting for an answer for 2 months, this question proposes the most creative solution:
iOS Present modal view controller on startup without flash
I guess it's time to accept that it's just not possible to present a view modally without animation before the root view controller appears. However the suggestion in that thread is to create an instance of your Launch Screen and leave that on for longer than default until the modal view controller has had a chance to present itself.
I guess it's time to accept that it's just not possible to present a view modally without animation before the root view controller appears.
Before it appears, no, you can't present. But there are multiple valid approaches to solve this visually. I recommend solution A below for its simplicity.
A. add launchScreen as subview, then present, then remove launchscreen
Solution is presented here by ullstrm and does not suffer from Unbalanced calls to begin/end appearance transitions:
let launchScreenView = UIStoryboard(name: "LaunchScreen", bundle: nil).instantiateInitialViewController()!.view!
launchScreenView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
launchScreenView.frame = window!.rootViewController!.view.bounds
window?.rootViewController?.view.addSubview(launchScreenView)
window?.makeKeyAndVisible()
// avoiding: Unbalanced calls to begin/end appearance transitions.
DispatchQueue.global().async {
DispatchQueue.main.async {
self.window?.rootViewController?.present(myViewControllerToPresent, animated: false, completion: {
launchScreenView.removeFromSuperview()
})
}
}
B. addChildViewController first, then remove, then present
Solution is presented here by Benedict Cohen.
I was in the same boat as you and found the same answer. I learned that you can get rid of the first problem (unbalanced calls warning) by setting the modalPresentationStyle of your setupViewController to .OverCurrentContext or .OverFullScreen. Problem solved - so I thought.
Only later I noticed the second problem and that was something I couldn't live with... back to square one.
As you, I wanted a solution with a normal view hierarchy and I didn't want to 'fake' something. I think the most elegant solution is switching your windows rootViewController on first dismissal of your setupViewController.
So, at launch, you set the setupViewController as the rootViewController (if needed):
var window: UIWindow?
var tabBarController: UITabBarController!
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
tabBarController = window!.rootViewController as! UITabBarController
if needsToShowSetup() {
let setupViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewControllerWithIdentifier("SetupViewController") as! SetupViewController
window?.rootViewController = setupViewController
}
return true
}
When setup is done you call a method in your appDelegate to switch to the 'real' rootViewController:
func switchToTabBarController() {
let setupUpViewController = window!.rootViewController!
tabBarController.view.frame = window!.bounds
window!.insertSubview(tabBarController.view, atIndex: 0)
let height = setupUpViewController.view.bounds.size.height
UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 1.0, initialSpringVelocity: 1, options: .allZeros, animations: { () -> Void in
setupUpViewController.view.transform = CGAffineTransformMakeTranslation(0, height)
}) { (completed) -> Void in
self.window!.rootViewController = self.tabBarController
}
}
I was after a 'cover vertical' dismiss animation. For crossfade and others, you could use UIView.transitionFromView(fromView: UIView, toView: UIView...). Hereafter you can present/dismiss your setupController the normal way, so your doneButton action could be something like this:
#IBAction func doneButtonSelected(sender: UIButton) {
if presentingViewController != nil {
presentingViewController!.dismissViewControllerAnimated(true, completion: nil)
} else {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.switchToTabBarController()
}
}
Actually, I implemented this through delegation with the appDelegate being the delegate the first time around.
I understand that I can set my root controller dynamically in applicationDidFinishLaunchingWithOptions: but I specifically want to present the setup screen modally, so that when the user is done with it, it dismisses and the true first view of the application is revealed.
I have two suggestions. One is to try doing this in viewDidAppear:. I tried it and although you do see the root view controller's view if you look carefully, you barely see it, and sometimes you don't see it at all if you blink:
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
[self presentViewController:[self.storyboard instantiateViewControllerWithIdentifier:#"setupViewController"] animated:NO completion:NULL];
}
Of course you'd need to add a flag so that you don't do that every time viewDidAppear: is called - otherwise you'll never be able to get back to this view controller at all! But that's trivial and I leave it as an exercise for the reader.
My other suggestion - and you have clearly thought about doing this - is to use a custom embedded (child) view controller instead. That works around the limitations of the whole "presentation" thing.
I'd launch and set up things dynamically, as you say, with the child view controller present if needed, configuring it all during the launch process. The child view controller's view would just cover the root view controller's view. So that's what the user would see as the app launches.
And then when the user's setup procedure is over and the user "dismisses" this view, you tear that view down, with animation, and remove the child view controller - revealing the root view controller's view underneath. The animation will make this all indistinguishable from the dismissal of presented view, even though it isn't really one.
I am updating our app to be compiled with xcode6/iOS8.
One issue I am running into is that when a modal view is presented. the underlying subview is removed. It is completely blacked out.. until the modal view is dismissed.. then it re-appears.
Has anyone run into this with iOS8? The same code has worked since iOS4.
Code:
PigDetailViewController *pigDetailViewController = [[PigDetailViewController alloc] initWithNibName:#"PigDetailViewController" bundle:nil];
self.navigationController.modalPresentationStyle = UIModalPresentationCurrentContext;
self.navigationController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentViewController:pigDetailViewController animated:YES completion:nil];
In iOS 8 they've added a new presentation style that behaves like UIModalPresentationCurrentContext in the circumstance you've described, it's UIModalPresentationOverCurrentContext. The catch here is that unlike with UIModalPresentationCurrentContext, you want to set the view controller to be PRESENTED with this presentation style, like so:
PigDetailViewController *pigDetailViewController = [[PigDetailViewController alloc] initWithNibName:#"PigDetailViewController" bundle:nil];
pigDetailViewController.modalPresentationStyle = UIModalPresentationOverCurrentContext;
self.navigationController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentViewController:pigDetailViewController animated:YES completion:nil];
Note that to support iOS 7 and below you'll likely need to check the OS version and decide how to present the view controller based on that.
edit: I'd like to add one more note to this. In iOS7 with UIModalPresentationCurrentContext, when the presented VC was dismissed, the underlying VC had its viewDidAppear method called. In iOS8 with UIModalPresentationOverCurrentContext, I've found the underlying VC does not have its viewDidAppear method called when the VC presented over top of it is dismissed.
Adding a point to BrennanR's answer.. even viewWillAppear doesn't call when the VC presented over top of it is dismissed.
I think you are misunderstanding how a modal view controller works.
When you present a view controller modally it will control the entire screen. It has an opaque background (normally black) and then draws its view on top of that.
So, if you set the view.backgroundColor to yellow (for example) it will have a yellow background. If you set it to clear then it will show through to the black background.
What I think you want is for the other view to "show through" so it looks like the modal view is sitting on top of it.
The best way I have found of doing this is to use this method...
// in the view controller that is presenting the modal VC
modalVC = // the modal VC that you will be presenting
UIView *snapshotView = [self.view snapshotViewAfterScreenUpdates:NO];
[modalVC.view insertSubView:snapshotView atIndex:0];
// present the modal VC
This will take a "screenshot" of the current view hierarchy and then place that snapshot underneath everything in the modal VC.
That way your black screen will be replaced by a screenshot of the previous view controller.
I have 2 ViewControllers that I use App delegate to switch them according to user interaction.
in AppDelegate.m I have:
- (void) switchViews
{
if (_viewController.view.superview == nil) {
[_window addSubview:_viewController.view];
[_window bringSubviewToFront:_viewController.view];
[viewController2.view removeFromSuperview];
} else
{
[_window addSubview:_viewController2.view];
[_window bringSubviewToFront:_viewController2.view];
[_viewController.view removeFromSuperview];
}
}
_viewController is for main view and _viewController2 is for glview(I am using isgl3d). The switch works but everytime I switch back to glview, I see duplicated view on top, which I suspect even main view is duplicated too.
Any idea how can I remove the view entirely so that I don't have this issue? Thanks!
You shouldn't be adding and removing the views like this, just change which controller is the root view controller of the window. Doing that make the new controller's view a subview of the window, and removes the old controller's view.
if ([self.window.rootViewController isEqual: _viewController]) {
self.window.rootViewController = viewController2;
}else{
self.window.rootViewController = viewController;
I found out how to do this after watching Stanford Coding Together:IOS.
Some critical info of VC that I am not aware of:
Everytime VC is instantiate, viewDidLoad is called once to setup all the important stuff like outlets and such. Then viewWillAppear and viewWillDisappear will be called for in between view swapping. Because it is called just a moment before view is shown to user, all the geometry setting like view orientation and size is set here.
so what I do is:
I addSubview in viewDidLoad, the do all the running setup in viewWillappear and viewWillDisappear.
one more note: view will remain there as long as the app still running.
anyway Thanks rdelmar for helping.
I'm creating a library which will add a view at the bottom of the application (when my library is integrated in application).
I'm using view controller's view's frame parameter to get the size of the view and calculation my library's view frame according and showing it.
The problem is that when navigation bar is there, my view is going still below the actual view visible. So, i want to know whether current view controller is based on navigation controller or not and whether navigation bar is visible in that view or not. how can I find that?
I'm late with the reply, but for other persons who try to do the same thing (like me :D).
This code may solve your problem:
id nav = [UIApplication sharedApplication].keyWindow.rootViewController;
if ([nav isKindOfClass:[UINavigationController class]]) {
UINavigationController *navc = (UINavigationController *) nav;
if(navc.navigationBarHidden) {
NSLog(#"NOOOO NAV BAR");
} else {
NSLog(#"WE HAVE NAV BAR");
}
}
UINavigationBar inherits from and has all the fine properties and behaviors of UIView and one of these properties is hidden.
So for your view, if you can get a handle to your navigation bar, all you need to do is check to see if hidden is YES or NO.
one way to do this would be to have a UINavigationController property or accessor (setter & getter) for your library so whoever makes use of the library can set the navigation controller and/or bar on your library's behalf.
Up to date check from a view controller context:
let navHidden = navigationController?.isNavigationBarHidden ?? true
if needsCloseButton || navHidden
{
// here add an alternative ways to get out since back button is not here, say add a close button somewhere