iOS custom UIViewController container - view lifecycle and dealloc - ios

I have written a custom view controller container that can replace the existing view controller (VC1) with a new view controller(VC2) i.e. a replace type segue rather than pop/push.
Everything worked fine until testing with iOS 8. It seems a second party I'm using causes a crash if the view controller is replaced before the previous view controller is dealloc
i.e. if [VC2 viewDidLoad] called before [VC1 dealloc].
It is my understanding that I have no control over the dealloc so this kind of behaviour is out of my control. However, I want to make sure that I am not causing this behaviour in the way I'm controlling my child view controllers.
The following is pseudo code for my view controller container:
- (void)replaceStackWithNewController:(UIViewController*)newVC {
for(UIViewController *vc in [self viewControllers]) {
[vc willMoveToParentViewController:nil];
if([vc isViewLoaded]
[[vc view] removeFromSuperview];
}
[vc removeFromParentViewController];
}
//self.viewControllers is my navigation stack
self.viewControllers = nil;
self.childVC = nil;
if(newVC != nil) {
self.childVC = newVC;
}
[self addChildViewController:newVC];
//this adds the view to a pre existing wrapper
[self addView:newVC.view ToWrapper:wrapper];
//add controller to stack
[self.viewControllers addObject:newVC];
[newVC didMoveToParentViewController:self];
}

Related

Root View Controller's view not in the Window Hierarchy

I have coded a login page which in my app delegate (under the method application:didFinishLaunchingWithOptions:) is set to be my root view controller with
[self.window setRootViewController:vc];
Where vc is the instantiation of my HomeVC.
This view loads correctly. I can enter in all of the data required for login just fine but as soon as I try and load another view I get
Warning: Attempt to present <RegisterVC: 0x7d07f520> on
<HomeVC: 0x7d374830> whose view is not in the window hierarchy!
I tried a little hack to be certain I got the top most view controller using the following code:
- (UIViewController*) topMostController
{
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
return topController;
}
but, as I suspected, it only ever returned the current view controller, which is also the root view controller. It never even enters the while loop.
I cannot understand how the view controller I'm trying to call from is not on the hierarchy when it's not only displaying correctly but is also the only view on the hierarchy at all.
Edit 1: I'll include the AppDelegate application:didFinishLaunchingWithOptions: method's section where I create and add the VC and the Navigation VC.
UIViewController *vc = (HomeVC *)[[HomeVC alloc] initWithNibName:NSStringFromClass([HomeVC class]) bundle:nil];
self.navigationController = [[UINavigationController alloc] initWithRootViewController:vc];
[self.navigationController setNavigationBarHidden:YES];
[self.window setRootViewController:vc];
//^^^This is the line I'm most suspicious of, is that correct?^^^
[self.window setBackgroundColor:[UIColor colorWithHexString:kDefaultBackgroundColor]];
[self.window makeKeyAndVisible];
you can get the root view controller of the window, which should be the navigation controller, and then get its top view controller.
try like this:if you are sure there is a viewController.presentedViewController then send includeModal as YES.
- (UIViewController *)topmostViewControllerFrom:(UIViewController *)viewController
includeModal:(BOOL)includeModal
{
if ([viewController respondsToSelector:#selector(selectedViewController)])
return [self topmostViewControllerFrom:[(id)viewController selectedViewController]
includeModal:includeModal];
if (includeModal && viewController.presentedViewController)
return [self topmostViewControllerFrom:viewController.presentedViewController
includeModal:includeModal];
if ([viewController respondsToSelector:#selector(topViewController)])
return [self topmostViewControllerFrom:[(id)viewController topViewController]
includeModal:includeModal];
return viewController;
}

UIViewController category not working on viewDidLoad

I'm using a custom rootViewController, and am using the code below to access it from any UIViewController
#implementation UIViewController(CustomRootVC)
- (CustomRootVC*)customViewController
{
UIViewController *parent = self;
Class customClass = [CustomRootVC class];
while ( nil != (parent = [parent parentViewController]) && ![parent customClass] )
{
}
return (id)parent;
}
#end
If I call self.customViewController on viewDidLoad I get nil. If I call it on willAppear I get the reference I expect.
I'm guessing this is something to do with the order I add the view controllers to my view controller container (i.e. viewDidLoad is called before the view controller has been added to customViewController and so it isn't a parent), but I can't spot anything obvious. I add the view controllers as follows:
- (void)addViewController:(UIViewController*)controller toWrapper:(PLSliderView*)wrapper{
[self addChildViewController:controller];
[self addView:controller.view ToWrapper:wrapper];
[self.viewControllers addObject:controller];
[controller didMoveToParentViewController:self];
}
In particular, the issue seems to be with adding a new view controller and view as follows:
- (void)replaceTopOfStackWithViewController:(UIViewController *)newController animated: (BOOL)animated {
UIViewController *oldController = self.currentController;
[self addChildViewController:newController];
[self transitionFromViewController:oldController
toViewController:newController
duration:1.0
options:UIViewAnimationOptionTransitionCrossDissolve
animations:nil
completion:^(BOOL finished) {
[self.rightViewController didMoveToParentViewController:self];
[self removeViewController:oldController];
[self queryDimensions:#"REPLACE"];
[self.view setUserInteractionEnabled:YES];
self.currentController = newController
}];
}
The view controller hasn't been added to the view controller hierarchy at the time viewDidLoad is called, hence it has no parent view controller, hence your function returns nil as expected.
this piece of code (parent = [parent parentViewController]) sets parent equal to self.parentViewController. If self does not have a parent view controller at the time this is called, it would return nil as expected.
more broadly, I'm not sure what you're trying to accomplish with this code, or why you need to use a category. it seems like that sort of behavior would make more sense in a UIViewController subclass.

How can a modal view controller be dismissed if the presenting view controller is changed?

I am presenting a modal view controller on the iPad which changes the presenting view controller while presented. For example:
A view controller VC presents the modal view controller when the user selects a cell in a table view.
The user selects an item on the modal view controller and another VC instance is opened in place of the first. Importantly, the view controller instance replacing the first is of the same type.
The modal view controller cannot be dismissed or an EXC_BAD_ACCESS exception occurs.
The failing dismiss is understandable: the presenting view controller is no longer available. Basically, how would I dismiss this presented modal view controller from a different presenting view controller?
The code I already have is:
ViewController1.m
- (void)showModalViewController:(UIViewController *)viewController
{
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
navigationController.modalPresentationStyle = UIModalPresentationFormSheet;
viewController.navigationItem.rightBarButton = [[UIBarButton alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:#selector(dismissModalViewController)];
[self presentViewController:navigationController animated:YES completion:nil];
}
- (void)dismissModalViewController
{
[self dismissViewControllerAnimated:YES completion:nil];
[self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:YES];
}
Thanks for your suggestions but I solved the issue by using delegation. The presented view controller defined a delegate to notify the presenter when an action occurred.
ChildViewControler.h:
#protocol ChildViewControllerDelegate <NSObject>
- (void) childView:(ChildViewController *)childView didSelectItem:(Item *)item;
#end
ChildViewController.m:
// in interface
#property (nonatomic, weak) id <ChildViewControllerDelegate> delegate;
// in implementation
- (void)closeView:(Item *)anItem
{
[self.delegate childView:self didSelectItem:anItem];
}
ViewController1.m:
- (void)showModalViewController:(UIViewController *)viewController
{
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
navigationController.modalPresentationStyle = UIModalPresentationFormSheet;
viewController.navigationItem.rightBarButton = [[UIBarButton alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:#selector(dismissModalViewController)];
// Different view controller types may be passed here so check is required...
if (viewController.class == [ChildViewController class]) {
((ChildViewController *)viewController).delegate = self;
[self presentViewController:navigationController animated:YES completion:nil];
}
- (void)childView:(ChildViewController *)childView didSelectItem:(Item *)item
{
[self dismissViewControllerAnimated:YES completion:nil];
[self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:YES];
// Perform action required with 'item'
}
You can try to change
self.presentingViewController
(The view controller that presented this view controller or its farthest ancestor.)
property in your modal View Controller before dismissing.
Here is your problem:
//...
viewController.navigationItem.rightBarButton = [[UIBarButton alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:#selector(dismissModalViewController)];
//...
- (void)dismissModalViewController
{
[self dismissViewControllerAnimated:YES completion:nil];
[self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:YES];
}
You try to dismiss presenter view controller (that is currently seems to be swithced to another already) instead of presented modal view controller (in your case it UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];)
so (if presenters view controllers not the tabs of tabViewController or not stored in navigationController's stack or somewhere else) you must to store reference to it somewhere else than in presenter view controller which will be switched and could be deallocated.
As per document presentingViewController is a readonly property.
You could not modify it.
#property(nonatomic,readonly) UIViewController *presentingViewController NS_AVAILABLE_IOS(5_0);
I have not tested this code but there may be error in dismissModalViewController.
please put break point on this method the first line is perfect may your second line may cause error,may self.tableView is not accessible or self.tableView indexPathForSelectedRow may be nil.
- (void)dismissModalViewController
{
[self dismissViewControllerAnimated:YES completion:nil];
[self.tableView deselectRowAtIndexPath:[self.tableView indexPathForSelectedRow] animated:YES];
}
Thanks.

UINavigationController not executing completionBlocks when presenting ViewController

This one is tricky. I have a subclass of UINavigationController that overrides pop/push and present/dismiss methods. Here I customise the behaviour to set the correct size if the UINavigationController subclass is contained in a popover. Nothing too fancy, but I do it this way to don't write subclasses of all my ViewControllers and use Autolayout.
However, the completion blocks of the presentViewController:animated:completion: and dismissViewControllerAnimated:completion: are not being executed. And this is the weird part: the exact same code on iPhone works correctly, but on iPad is not executing the blocks. Here is a code sample.
#interface SBNavigationController : UINavigationController
#end
#implementation SBNavigationController
- (void) presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion
{
if ([viewControllerToPresent isKindOfClass:[UINavigationController class]])
{
UINavigationController *nav = (UINavigationController *) viewControllerToPresent;
[nav.topViewController setContentSizeForViewInPopover:kFullSizePopover];
} else
{
[viewControllerToPresent setContentSizeForViewInPopover:kFullSizePopover];
}
viewControllerToPresent.modalPresentationStyle = UIModalPresentationCurrentContext;
[super presentViewController:viewControllerToPresent animated:flag completion:completion];
}
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion ;
{
[super dismissViewControllerAnimated:flag completion:completion];
}
#end
And the code using it is this:
#implementation SBInviteFBContactViewController
...
- (void) createInviteByMailViewController
{
SBInviteMailViewController *mailInvite = [[SBInviteMailViewController alloc] initWithDelegate:self userInfo:_userInfo];
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:mailInvite];
[self.navigationController presentViewController:navController
animated:YES
completion:^{
NSLog(#"presentViewController:");
}];
}
#pragma mark SBInviteMailProtocol
- (void) invitedMailContacts:(NSArray *)contacts;
{
[self.navigationController dismissViewControllerAnimated:YES
completion:^{
NSLog(#"animation Ended");
if (contacts) {
[self.delegate invitedMailContact:contacts];
[self popViewControllerAnimated:YES];
}
}];
}
...
#end
Any ideas?
This seems to be a huge bug. Please report it to Apple (and I am about to do the same). I found my way here because I just discovered the same bug myself, and did a google search to see if anyone else was talking about it.
I've created a very small demonstration project, whose architecture is like this:
ViewController - the main view controller
Its view contains a button Tap Me.
PopoverViewController - presented in popover
When you tap Tap Me in the main ViewController, it creates a UIPopoverController with this vc, PopoverViewController, as its content view controller; its view, too, contains a button Tap Me.
PopoverViewController2 - presented "modally" in same popover
PopoverViewController2 has its modalPresentationStyle set to UIModalPresentationCurrentContext so it can appear inside the popover. When you tap Tap Me in the popover, PopoverViewController calls presentViewController:....
Here's the code:
- (IBAction)doTapMe:(id)sender {
NSLog(#"about to present view controller");
[self presentViewController:[PopoverViewController2 new] animated:YES completion:^{
NSLog(#"in completion handler"); // never called!
}];
NSLog(#"did present view controller");
}
The log reads "about to present view controller" and "did present view controller", but "in completion handler" never appears, even though the "modal" view controller's view appears in the popover just fine.
(Moreover, changing to animated:NO not only doesn't fix it, it causes a visual glitch.)
The UIModalPrsentationCurrentContext style is only available if you are compiling against iOS 3.2 or greater. Can't imagine that is the issue though.
The docs for UIModalPrsentationCurrentContext also say:
When presenting a view controller in a popover, this presentation style is supported only if the transition style is UIModalTransitionStyleCoverVertical. Attempting to use a different transition style triggers an exception. However, you may use other transition styles (except the partial curl transition) if the parent view controller is not in a popover.
This is a strange one.
Any chance you're running a different version of iOS on the iPhone and the iPad?

Issue with definesPresentationContext / UIModalPresentationCurrentContext - Current context view controller gets lost

This only accours if you are presenting in a view controller that is managed by a navigation controller.
The reproduction steps are:
1 - Present a view controller using UIModalPresentationCurrentContext
self.definesPresentationContext = YES;
ViewController* viewController = [[ViewController alloc] init];
viewController.modalPresentationStyle = UIModalPresentationCurrentContext;
[presentOnViewController presentViewController:viewController animated:YES completion:nil];
2 - Present a view controller over the top using the default full screen presentation style
ViewController* viewController = [[ViewController alloc] init];
[self presentViewController:viewController animated:YES completion:nil];
3 - Dismiss the top presented view controller (the full screen one)
[self dismissViewControllerAnimated:YES completion:nil];
Now the problem is the 2nd view controller (presented using UIModalPresentationCurrentContext) disappears. Also it is impossible to present another view controller using UIModalPresentationCurrentContext, because the system thinks its still there.
I believe the issue is a bug in the framework. As mentioned it only occurs when the presenting in a view controller managed by a navigation controller. There is a nasty work around which uses the containment API. It creates a dummy view controller which views are presented from. The steps are:
1 - When presenting a view in context who's parent is a navigation controller, use a dummy view controller:
- (void)presentInContext
{
UIViewController* presentOnViewController = self;
if ([self.parentViewController isKindOfClass:[UINavigationController class]])
{
// Work around - Create an invisible view controller
presentOnViewController = [[DummyViewController alloc] init];
presentOnViewController.view.frame = self.view.frame;
// Containment API
[self addChildViewController:presentOnViewController];
[self.view addSubview:presentOnViewController.view];
[presentOnViewController didMoveToParentViewController:self];
presentOnViewController.definesPresentationContext = YES;
}
ViewController* viewController = [[ViewController alloc] init];
viewController.modalPresentationStyle = UIModalPresentationCurrentContext;
[presentOnViewController presentViewController:viewController animated:YES completion:nil];
}
2 - When dismissing the view controller tidy up
- (void)dismissSelf
{
__weak UIViewController* presentingViewController = self.presentingViewController;
[self dismissViewControllerAnimated:YES completion:^{
// Remove the dummy view controller
if ([presentingViewController isKindOfClass:[DummyViewController class]])
{
[presentingViewController willMoveToParentViewController:nil];
[presentingViewController.view removeFromSuperview];
[presentingViewController removeFromParentViewController];
}
}];
}
Thats it... The fix is dirty, but does the trick with no visual flicker.

Resources