iOS5 Update
New in iOS5, the use and meaning of many of the existing
UIViewController
properties has changed as part of the new container view controller framework that is being made available. As a result, some of the information in this article is no longer valid. In particular, theparentViewController
property is no longer populated on a presented modal view controller, making the method suggested here to dismiss that way ineffective. Instead, applications should use thepresentingViewController
property to dismiss from within the presented context in iOS5, such as:[[self presentingViewController] dismissModalViewControllerAnimated:YES];
There is also a new version of these methods that takes a completion block (
presentViewController:animated:completion:
anddismissViewControllerAnimated:completion:
), which is the overall recommended method to use in the future.
One of the most common design patterns used to transition from one view to another
in iOS is the use of the modalViewController
property of
UIViewController.
New views are presented using the presentModalViewController:animated:
method,
and dismissed using the dismissModalViewControllerAnimated:
method. Presenting
a new view as a modal view does a number of things that are not always obvious:
- Creates a parent/child relationship between the two view controller objects
- The new view controller object is retained by the parent
- Displays the new view fullscreen (on iPhone), optionally in an animated fashion
- If animated, the animation is taken from the
UIModalTransitionStyle
of the NEW view
I have highlighted the top two items because it is those two that lead to the primary issues of misuse with modal presentation: over retention and child dismissal.
Child Dismissal
Per the Apple documentation:
The parent view controller is responsible for dismissing the modal view controller it presented using the
presentModalViewController:animated:
method. If you call this method on the modal view controller itself, however, the modal view controller automatically forwards the message to its parent view controller.
Apple has done the developer a disservice here by automatically correcting bad practice,
but doing so doesn't make it good practice. Apple clearly states that presentation
AND dismissal of modal views are the responsibility of the parent view controller.
dismissModalViewControllerAnimated:
should never be called from the child (modal) view;
despite the fact that iOS helps you out and autoforwards the request to the parent.
The reason is because the second sentence is ONLY true when the modal view is the
final child of a modal view stack. If a UIViewController
has a modalViewController
set,
then a dismiss message will result in that controller dismissing its child.
Every view controller that has been presented in this way (it is a child) has a
property called parentViewController
that is set, making it very easy for the
developer to pass the dismiss message on to the proper object, even if they are
not in that context at the moment. See the example below, where versions of the
right and wrong way to dismiss from a child are displayed:
@implementation ViewControllerOne
- (void)showNewView {
ViewControllerTwo *controller = [[ViewControllerTwo alloc] init];
[self presentModalViewController:controller animated:YES];
[controller release];
}
- (void)dismissRight {
//Work is done...dismiss the child
[self dismissModalViewControllerAnimated:YES];
}
@end
@implementation ViewControllerTwo
- (void)dismissWrong {
//Work is done...dismiss me
[self dismissModalViewControllerAnimated:YES];
}
- (void)dismissRight {
//Work is done...dismiss me
[[self parentViewController] dismissModalViewControllerAnimated:YES];
}
@end
In this example, both places where dismissRight
exists are proper ways of dismissing
a modal, whether from the parent or child. Using the technique in dismissWrong
will
only get you into trouble some day.
Consider a case where a developer makes several calls to presentModalViewController:animated:
,
creating a view controller stack. Every view controller in that stack has their
parentViewController
and modalViewController
properties set...except for the
top-level and bottom-level controllers, whom have the parentViewController
and
modalViewController
properties set to nil
respectively. If a developer has the
improper concept in their heads of where to send a dismiss message, messages sent
to view controllers in the middle of the stack (which is allowed) would dismiss the
child of the receiver, instead of the receiver itself. This would lead to major
troubles in debugging because the design concept was built on flawed logic.
JUST DON'T DO IT.
Over Retention
The other common problem arises from the fact that developers may not realize that
presenting a modal view controller (through the act of setting it as the modalViewController
property) causes it to be retained by the parent. Therefore, you do not need to
create properties in your parent classes to hold instances of the modal:
@interface MyViewController : UIViewController {
ChildViewController *modalVC;
}
@end
@implementation MyViewController
- (void)viewDidLoad {
[super viewDidLoad];
modalVC = [[ChildViewController alloc] init];
}
- (void)showNewView {
[self presentModalViewController:modalVC animated:YES];
}
@end
In fact, this will keep the object from being released once dismissed, and you will be holding data from one use of the modal to the next. If you want to make sure your modal view controller loads up fresh with each presentation, rely only on the retain message sent by the parent:
@implementation MyViewController
- (void)showNewView {
ChildViewController *modalVC = [[ChildViewController alloc] init];
[self presentModalViewController:modalVC animated:YES];
[modalVC release];
}
@end