I have a view controller in a storyboard that is using a container view. Both have restoration identifiers set. The parent is being saved and restored just fine. The child however is not. Neither -encodeRestorableStateWithCoder: or -decodeRestorableStateWithCoder: are being called on the child view controller.
What's the correct way to save child view controllers that are created with a view container? I can save the child view controller in the parents -encodeRestorableStateWithCoder:, which will cause it to be saved, but I don't have a way of using it during a restore.
Container view controller "does not automatically save references to any contained child view controllers. If you are implementing a custom container view controller, you must encode the child view controller objects yourself if you want them to be preserved".
There are simple rules that i found:
1.Embedded(child) view controller should already be created and added to parent view controller at the state preservation process. So, do not have to do anything if you use storyboard otherwise you'll have to instantiate child view controller and add it manually:
-(void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"Did load");
MyChildViewController *childViewController = [MyChildViewController new];
[self addChildViewController:childViewController];
[childViewController didMoveToParentViewController:self];
self.childVC = childViewController;
}
You can add child view at -viewDidLoad or later. Use self.childVC.view.frame = [self frameForChildController]; [self.view addSubview:self.childVC.view]; for this.
2.You no need to save the child view controller in the parent's -encodeRestorableStateWithCoder: himself, but you should encode a reference to that object using -encodeObject:forKey:. If you have reference you can do it like this:
-(void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
NSLog(#"Encode");
UIViewController *childViewController = self.childVC;
[coder encodeObject:childViewController forKey:#"ChildVC"];
[super encodeRestorableStateWithCoder:coder];
}
see https://stackoverflow.com/a/13279703/2492707 to get reference to child VC if you use Storyboard. Or you can write something simple like this:
-(void)encodeRestorableStateWithCoder:(NSCoder *)coder
{
NSLog(#"Encode");
UIViewController *childViewController = [self.childViewControllers objectAtIndex:0]; //[self.childViewControllers lastObject];
[coder encodeObject:childViewController forKey:#"ChildVC"];
[super encodeRestorableStateWithCoder:coder];
}
3.Embedded(child) view controller should already be created and added to parent view controller at the state restoration process. So, if you did everything in the first paragraph, there is nothing more to do here.
4."In this case, however, we do not decode child view controller. We could, but in fact we don't need it.The MyChildViewController object will restore its own state. We only encoded this reference in order to get the runtime to walk the chain down to the MyChildViewController instance and do save-and-restore on it".
-(void)decodeRestorableStateWithCoder:(NSCoder *)coder
{
NSLog(#"Decode");
[super decodeRestorableStateWithCoder:coder];
}
This book helps me for understanding state preservation with container views. Also look for a good example for this book
I think the answer is in the documentation
It is said:
" The UIViewController class saves a reference to the presented view controller and the storyboard (if any) that was used to create the view controller. The view controller also asks the views in its view hierarchy to save out any relevant information. However, this class does not automatically save references to any contained child view controllers. If you are implementing a custom container view controller, you must encode the child view controller objects yourself if you want them to be preserved."
So you could do something like that:
-(void)encodeRestorableStateWithCoder:(NSCoder *)coder {
[super encodeRestorableStateWithCoder:coder];
[self.myChildViewController encodeRestorableStateWithCoder:coder];
}
-(void)decodeRestorableStateWithCoder:(NSCoder *)coder {
[super decodeRestorableStateWithCoder:coder];
[self.myChildViewController decodeRestorableStateWithCoder:coder];
}
And in MyChildViewController do not call super :)
Related
I want to create a custom segue that acts in the same way as the standard push segue does when used on UINavigationController view controllers. I've implemented my custom segue:
CustomSegue.m
-(void)perform {
UIViewController *source = (UIViewController *)[self sourceViewController];
UIViewController *dest =(UIViewController *)[self destinationViewController];
if (1==2 //test) {
[source.navigationController pushViewController:dest animated:YES];
}
else {
UIViewController *altDest = [[UIStoryboard storyboardWithName:#"MainStoryboard" bundle:NULL]
instantiateViewControllerWithIdentifier:#"alternateView"];
[source.navigationController pushViewController:altDest animated:YES];
}
As you can see, the reason I want to use a custom push segue is so that I can decide which view controller to push based on the user configuration (currently only checking a trivial 1==2 expression). I can instantiate the alternate view controller with no issue, but what I want to be able to do is go back and forth without reloading the view controller each time (using the back and next buttons). Is there a way to retrieve an existing instance from the storyboard, or some standard way of doing this?
Instead of a custom segue with its perform, the way to do what you describe, i.e. choose in real time whether to push dest or altDest, is either (1) do not use segues at all and just call pushViewController directly as you are doing here, or (2) prepare two segues emanating from the view controller as a whole, and call performSegueWithIdentifier: to say which we should perform.
As for going directly from dest to altDest, you can push altDest on top of dest and then remove dest from the stack of the navigation controller's view controllers.
Like so much about about iOS, this is all so much easier and more obvious if you do not use a storyboard at all. This is why I don't like storyboards: they are so simple-minded and limiting, and they distract one's attention from the way iOS really works.
There is no way to retrieve an existing controller from a storyboard -- it would be nice if there were a controllerWithIdentifier: method to do that, but there isn't. Segues (other than unwinds) always instantiate new controllers, so I don't think you can do what you want with a segue. If you want to be going forward (push) to the same controller multiple times, then you need to do it in code by creating a property that points to your controller, and checking if that controller exists before pushing to it.
As the others have pointed out, you can't use a segue to push to an existing instance of a controller. The process of performing a segue always creates a new instance the destination controller for you.
Personally, when I'm jumping between existing instances of view controllers, I think "container view controller", such as a UIPageViewController, which makes it really easy to transition between two or more controllers, without necessarily reinstantiating them every time.
If you don't like the constraints the page view controller imposes (e.g. maybe you don't like the fact that iOS 5 version only supports page curl transitions, or that iOS 6 only adds the scroll transition, and you want something else), then you'd do a custom container view controller.
For example, if I wanted to jump between two view controllers and not reinstantiate them every time, I'd first create a custom container view controller, the "parent", and make sure I have a property to keep track of which child I'm currently at:
#property (nonatomic) NSInteger childViewIndex;
If supporting iOS 6.0 and above only, I'd then add a "container view" to my parent view controller's scene. If supporting iOS versions prior to 6.0, I'd add a standard UIView to the scene and then manually instantiate the first child controller:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
UIViewController *controller;
// add the first child
UIViewController *controller = [self addChildWithIdentifier:#"One"];
[self.containerView addSubview:controller.view];
[controller didMoveToParentViewController:self];
self.childViewIndex = 0;
}
- (UIViewController *)addChildWithIdentifier:(NSString *)storyboardIdentifier
{
UIViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:storyboardIdentifier];
[self addChildViewController:controller];
controller.view.frame = self.containerView.bounds;
controller.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
return controller;
}
Then, when I want to transition to the second child (or transition back to the first child), I'd call the following routine in the parent view controller:
- (void)transitionToViewControllerIndex:(NSInteger)index
{
// don't do anything if we're trying to transition to ourselves!
if (index == self.childViewIndex)
return;
// identify the two controllers in question
UIViewController *sourceController = self.childViewControllers[self.childViewIndex];
UIViewController *destinationController;
// if we're asking for page 2, but we only have one defined, then we'll have to instantiate it
BOOL instantiateDestination = (index == 1 && [self.childViewControllers count] < 2);
if (instantiateDestination)
destinationController = [self addChildWithIdentifier:#"Two"];
else
destinationController = self.childViewControllers[index];
// configure the destination controller's frame
destinationController.view.frame = sourceController.view.frame;
// if you're jumping back and forth, set the animation appropriate for the
// direction we're going
UIViewAnimationOptions options;
if (index > self.childViewIndex)
{
options = UIViewAnimationOptionTransitionFlipFromRight;
}
else
{
options = UIViewAnimationOptionTransitionFlipFromLeft;
}
// now transition to that destination controller
[self transitionFromViewController:sourceController
toViewController:destinationController
duration:0.5
options:options
animations:^{
// for simple flip, you don't need anything here,
// but docs say this can't be NULL; if you wanted
// to do some other, custom annotation, you'd do it here
}
completion:^(BOOL finished) {
if (instantiateDestination)
[destinationController didMoveToParentViewController:self];
}];
self.childViewIndex = index;
}
Thus, to transition to the second child view controller, you could simply call:
[self transitionToViewControllerIndex:1];
If you want to transition back, you could call:
[self transitionToViewControllerIndex:0];
I'm only scratching the surface here, but container view controllers (or if none of the standard ones do the job for you, a custom container view controller) is precisely what you need.
For more information, see:
Creating Custom Container View Controllers in the View Controller Programming Guide for iOS.
Implementing UIViewController Containment in the WWDC 2011 Session Videos (Apple developer ID required).
Implementing a Container View Controller in the UIViewController Class Reference.
Page View Controllers in the View Controller Catalog for iOS.
I am trying to detect when a ViewController was pushed.
So I followed the doc of Apple http://developer.apple.com/library/ios/#documentation/uikit/reference/UINavigationBarDelegate_Protocol/Reference/Reference.html , about NavegationBar delegate but I didnĀ“t figured out how to make it working successfully.
I placed on my code the following code in my ViewController but it doesn't detect it was pushing.
What I am doing wrong ?
- (void)navigationBar:(UINavigationBar *)navigationBar didPushItem:(UINavigationItem *)item, {
NSLog(#"didPushItem: %#", item);
[self showimage];
}
Not clear what you are needing to do but there are several UIViewController methods for discerning its context. There are two below and a couple more in the docs
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
BOOL pushed = [self isMovingToParentViewController];
printf("viewWillAppear %d\n", pushed);
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
BOOL popped = [self isMovingFromParentViewController];
printf("viewWillDisappear %d\n", popped);
}
You should implement UINavigationControllerDelegate for UIViewController and UINavigationController related tasks.
Here is the link to the documentation:
http://developer.apple.com/library/ios/#documentation/UIKit/Reference/UINavigationControllerDelegate_Protocol/Reference/Reference.html
The particular method you want, which would be something like "navigationController:didPushViewController:animated:", does not exist in the protocol.
However, I believe you can achieve the desired behavior using the navigationController:willShowViewController:animated:. Note that this method gets called before the View for the UIViewController is shown and after it has been pushed into the UINavigationController stack.
The -viewWillApear method is reasonable, but it gets called when the view is about to be inserted into the view hierarchy, which may or may not be what you want.
If you want more control of the push/pull progress, you can override
- (void)willMoveToParentViewController:(UIViewController *)parent {
if (nil == parent) {
// Moving to nil parent means being removed from parent
} else {
// Will be inserted as a child view controller of <parent>
}
}
- (void)didMoveToParentViewController:(UIViewController *)parent {
if (nil == parent) {
// Moving to nil parent means was just removed from parent
} else {
// Was just inserted as a child view controller of <parent>
}
}
These will be called just before and after the navigation controller pushes/pops the child view controller.
From the docs...
didMoveToParentViewController:
Called after the view controller is added or removed from a container view controller.
- (void)didMoveToParentViewController:(UIViewController *)parent
Parameters
parent
The parent view controller, or nil if there is no parent.
Discussion
Your view controller can override this method when it wants to react
to being added to a container.
and...
willMoveToParentViewController:
Called just before the view controller is added or removed from a
container view controller.
- (void)willMoveToParentViewController:(UIViewController *)parent
Parameters
parent
The parent view controller, or nil if there is no parent.
Discussion
Your view controller can override this method when it needs to know
that it has been added to a container.
As far as I understand the SDK documentation UIViewController's navigationItem lifecycle is bound to the controller itself and not to the controller's view. I.e. in the default implementation it is created on-demand and destroyed with the view controller - with all contents like button items and titleView. Given that both button items and the titleView may be represented by UIView instances - does that mean that once created these views will stay in memory until controller is destroyed and live through all memory warnings?
What is the sense behind this design decision? Is impact for memory usage considered too small to bother? Is it really small for an application which is using customized nav bar buttons/titles everywhere?
It is easy to explicitly bound some of the navigationItem properties to the controller's view lifecycle - like setting titleView in -viewDidLoad and dropping it in -viewDidUnload (self.navigationItem.titleView = nil). But the navigationItem property documentation suggests to avoid this pattern. Are there any other potential problems other than the given example with back button?
Added a category (snippet2) to track the retain count and the destruction of the navigation items, feel free to do the same :) Seems like it is not deallocated with the memory warning. An explanation would come from a common sense that view controllers don't have to be used with the navigation controller: that should be why the nav-item is added with a separate category (snippet1) and it's lifecycle must be managed with a nav-controller, not the view controller instance itself.
In the case the custom nav-items are so heavy that you need to release it whenever possible,
i would leave the default implementation, add custom nav-items category and manage this items manually as i wish (again through overriding required UINavigationController methods like nav-controllers didReceiveMemoryWarning, pushViewController:animated:, popViewControllerAnimated:animated:). I can't imagine such a case when it is really needed however.
snippet 1
#interface UIViewController (UINavigationControllerItem)
#property(nonatomic,readonly,retain) UINavigationItem *navigationItem; // Created on-demand so that a view controller may customize its navigation appearance.
#property(nonatomic) BOOL hidesBottomBarWhenPushed; // If YES, then when this view controller is pushed into a controller hierarchy with a bottom bar (like a tab bar), the bottom bar will slide out. Default is NO.
#property(nonatomic,readonly,retain) UINavigationController *navigationController; // If this view controller has been pushed onto a navigation controller, return it.
#end
snippet 2
#implementation UINavigationItem (Logs)
- (id)init
{
NSLog(#"I'm initialized (%#)", [self description]);
self = [super init];
return self;
}
-(void) release
{
NSLog(#"I'm released [%d](%#)", [self retainCount], [self description]);
[super release];
}
-(void) dealloc
{
NSLog(#"I'm deallocated [%d](%#)", [self retainCount], [self description]);
[super dealloc];
}
#end
I have a UIViewController that is creating another view controller, and adding its view as a subview:
In the parent UIViewController:
SlateMoreView* subView = [[SlateMoreView alloc] initWithNibName:#"SlateMoreView" bundle:nil];
[self.view addSubview:subView.view];
I then need to call a method from the subview, in the parent view.
I have seen how to do this when I am adding the sub UIViewController using [self.navigationController pushViewController: subView animated: YES], because I can find the parent using this kind of code:
In the sub view UIViewController:
NSArray* viewControllerArray = [self.navigationController viewControllers]
int parentViewControllerIndex = [viewControllerArray count] - 2;
SlateView* slateView = [viewControllerArray objectAtIndex:parentViewControllerIndex];
...and then I can send messages to it. But since I added the sub view manually by using addSubView, I can't do this.
Can anyone think of how I can talk to my parent UIViewController?
Thanks!
UIViews have a superview property which seems to be what you are looking for.
In addition you probably don't want to nest UIViewController's view like that unless you are very deliberately building a custom contain view controller. See http://blog.carbonfive.com/2011/03/09/abusing-uiviewcontrollers/
You might want to consider if your problem can be solved by using NSNotifications. You could post a notification from your subview when an event happens that interested listeners (your superview) need to know about . When the superview receives the notification, it can run whatever code you wish. All the while the subview never needs to know about the superview.
This is one way to make your classes less dependant on each other.
You could also use delegation as another option.
When you add your view as a subview to a view hierarchy, you put it in the responder chain. You can go up the responder chain to reach the view controller as a UIView controlled by a UIViewController has the UIViewController as its nextResponder.
id object = theSubview;
do {
object = [object nextResponder];
} while ( ![object isMemberOfClass:[YourViewController class]] );
// object has the view controller you need.
I need to add this to my dismiss button :-
[self dismissModalViewControllerAnimated:YES];
[self release];
else
[self.view removeFromSuperview];
I thought
if( self.navigationController.modalViewController ) {
would work be it nevers true
A couple of things:
1) You shouldn't ever release yourself in an object. If you're presenting a modal view controller, you should perform the release there since the view controller will now be retained by the view controller's .modalViewController property:
(In the parent):
UIViewController *someViewController = [[UIViewController alloc] init];
[self presentModalViewController:someViewController animated:YES];
[someViewController release];
2) The parent will store its child modal view controller in .modalViewController. The child will have its .parentViewController property set in this case. If the view has been added as a subview, its .superview property will be set. These are not mutually exclusive, however, so be careful. Generally speaking, UIViewControllers are intended to host full-screen views, and if you're adding the view as a subview, you should ask yourself if the view should just be a UIView subclass, and move the logic into the parent view controller.
That said, I suppose you could check your case (assuming you don't present modal view controller and add as a subview simultaneously):
if (self.parentViewController) {
[self dismissModalViewControllerAnimated:YES];
} else if (self.view.superview) {
[self.view removeFromSuperview]
}
In the latter superview case, the view controller will still be hanging around, so you'd need to let the other view controller know via delegate method or something to release you. In the first case, if you have released the presented view controller already as I described above, it will be released automatically when the parent view controller sets its .modalViewController property to nil.
Normally for a "dismiss" button I would call a method in the controller that presented the modal controller (use a delegate), not try to dismiss the modal view controller from within itself. I don't quite get what youre trying to do though, but that [self release] looks bad. I don't think you ever want to release self like that.
Try this in you modal viewcontroller:
- (IBAction)close:(id)sender {
[self.parentViewController dismissModalViewControllerAnimated:YES];
}
Then just connect the button's action to that method.