Transition between view controllers and rotation in iOS - iphone

Consider a container view controller with two child view controllers (A and B), both added with addChildViewController:. Then:
A.view is added to the container view
B is displayed by doing transitionFromViewController from A to B. B receives viewWillLayoutSubviews and all is good with the world.
The device rotates while displaying B. Only B receives the rotation calls (willRotateToInterfaceOrientation: et all).
A is displayed by doing transitionFromViewController from B to A. A doesn't receive viewWillLayoutSubviews and thus the layout is broken.
Is this the expected behavior? If not, what might I be doing wrong? If yes, what should I do to notify A of the rotation change while displaying B?

As soon as you call addChildViewController: you are now a View Controller Container Implementer. This means you do have to do a little more work than a standard presentation call like presentViewController... This includes dealing with the frames of the views of the controllers you add as children, as your question suggests you might have expected.
For example, to implement a super basic example container, that just shows each child full screen, you could do something like this.
-(void)swapChildVCFrom:(UIViewController *)from to:(UIViewController *)to{
[self addChildViewController:to];
[from willMoveToParentViewController:nil];
// Adjust the new child view controller's view's frame
// For example here just set it to fill the parent view
to.view.frame = self.view.bounds;
[self transitionFromViewController:from
toViewController:to
duration:1.0
options:UIViewAnimationOptionTransitionFlipFromLeft
animations:nil
completion:^(BOOL b){
[to didMoveToParentViewController:self];
[from.view removeFromSuperview];
[from removeFromParentViewController];
}];
}

After A has been loaded and displayed, viewWillLayoutSubviews will not be called again unless A.view's bounds change. If you need your implementation to run each time the view comes on screen, you can try moving that code to viewWillAppear.

There are three solutions that I can think of:
Add all of your container's view controller's up front, and add their views, but hide all but the first one. Then in your method that transitions between the view controllers, you don't have to worry about the myriad containment related methods anymore (e.g. you don't need willMoveToParentViewController, addChildViewController, removeFromParentViewController, or didMoveToParentViewController), but rather just use the UIViewAnimationOptionShowHideTransitionViews option which will show or hide the appropriate views. This way, all the controllers get all the rotation notifications.
The second logical alternative would be to create only the first view controller up front. Then, when you want to transition to the second view controller, only then would you create it (and it's view). And make sure you don't keep a strong reference to the original controller (e.g. not only your own properties, but make sure to removeFromParentViewController, etc.). That will let it, and it's views, be deallocated. Thus, when you transition back, it will be recreated.
If you're creating your view controllers up front, but letting transitionFromViewController to add and remove their respective views to your container, then I think you'll probably have to invoke the appropriate child rotation events, like discussed in the Customizing Appearance and Rotation Callback Behavior in the View Controller Programming Guide.

Related

How could a UIVIew know its child has committed suicide

I have a quick question:
View Controller X has added a sub-view: View Controller Y.
Y has a "quit" button. When the button is tapped, it removes its view from X's view hierarchy.
But I found Y's 'viewDidUnload' method is not called and its memory can't be released. How could Y release itself properly, or how could X know that Y is gone?
I believe I can make a protocol and let Y call a method from X, but I hope there is a easier way.
So you want the Y view controller to get cleaned when its view is closed?
Prior to iOS5 there's nothing fancy to support more than one UIViewController on screen at once, so you basically how to make the calls yourself.
You could notify X when Y is closed (you could use NSNotificationCenter, or a delegate), and X would set Y's view to nil which would cause the viewDidUnload method to be called in it.
Additionally you could even release the Y view controller, which would dealloc every instance it has (along with the view itself) if you're not going to show it again.
As you have added View Controller Y in View Controller X by coding...
[viewX.view addSubView:viewY.view];
So, the viewDidLoad method wont going to execute. And, as far as memory management is concerns, you can code for that in viewX.m, i.e. as the "quit" has been tapped...
[viewY removeFromSuperView];
[viewY released];
If you only need to know when a subview has been removed from its parent view, implement didAddSubview: and/or willRemoveSubview:. (See here in the documentation)
If you want to release the viewController when removing the view, you need to... release the viewController when you remove the view: [viewController.view removeFromSuperview]; [viewController release]; viewController = nil;
The viewDidUnload method is only called when there is a memoryWarning being issued by iOS, and that the ViewController then automatically unload its view (if it is not onscreen, e.g. the ViewController is not the topmost of a NavCtrl's stack so its view is not visible) to release some memory (and will then re-load the view from your XIB and recall viewDidLoad when it needs to reload the view to display it again).
You should not have 2 UIViewControllers on screen at the same time. There is a whole range of problems associated with that and it is not how they are intended to be used.
A single UIViewController should manage all subviews associated with a single screen. If you want a "child" view subclass UIView and add it as a subview.
To allow to switch between view controllers easily, add a UINavigationController as the root view controller, and push/pop view controllers via that. You can then change screens very easily. Remember - you can turn of the navbar and/or animation you get no visual indication that your app is using a navigation controller, but full benefit of it's ability to manage a stack of views and ensure all the correct notifications are sent to each view controller as it loads/appears/disappears/unloads.
If you get in the habit of building every app with a UINavigationController as the root view controller things become very simple.

a question about rotation when multiple views are involved

I'm a relative newcomer to cocoa & programming for the ipad.
I've built an app that has a split view controller. In the detail view is a toolbar with a button on it. When the button is pressed, the split view controller is removed from the superview, and another view is put in its place. A toolbar button on this new view removes the view and puts the split view back. Works great... except when the ipad is rotated while the second view is visible. When the user returns to the split view, it's displayed as it was before the rotation.
The split view and all the sub views are set to autoresize=yes, and return yes when they receive the autorotatetointerfaceorientation message.
I'm guessing I need to tell the split view and its sub views to resize themselves when I add it as a subview to the window.
Thanks
Chris
Please see my question concerning this matter here:
Best way to switch between UISplitViewController and other view controllers?
If you use UISplitViewController as Apple intend you to, it's quite limited.
I ended up using a strategy exactly as you mention -- i.e. remove the UISplitViewController's view from UIWindow, and replace with another, and then later switch back. I found out that the orientation change WAS handled, even if I rotated while view B was presented (B being the non-split view), then switch back to A (the split view). However, I had to do a bit of fiddling with the frame size of the uisplitview to make it work. Will update with more info later when I find it.
There's also the option of writing your own split view controller, or using someone else's reimplementation, such as this one:
http://mattgemmell.com/2010/07/31/mgsplitviewcontroller-for-ipad
UPDATE
The fiddling I did with the frame size of UISplitView can be seen in the following method in my AppDelegate. These methods are for presenting the split view controller by replacing another top level view controller under UIWindow:
- (void)removeAllWindowSubviews {
for (UIView *childView in window.subviews) {
[childView removeFromSuperview];
}
}
- (void)presentSplitView:(UISplitViewController *)vc {
[self removeAllWindowSubviews];
UIView *viewForSplitVC = vc.view;
// fix for deficiency in adding a split view controller's view in landscape mode
// and it still having a frame for portrait mode.
// 2010-10-15 added -20.0f to fix problem with toolbar in LHS VC being 20 pix too low.
viewForSplitVC.frame = CGRectMake(viewForSplitVC.frame.origin.x, viewForSplitVC.frame.origin.y,
navigationController.view.bounds.size.width, navigationController.view.bounds.size.height - 20.0f);
[window addSubview:viewForSplitVC];
}
// for removing the split view and restoring the other main VC
- (void)restoreMenu {
if (isIPad()) {
[self removeAllWindowSubviews];
[window addSubview:navigationController.view];
}
}
As I said, it's a hack, but the correcting of the frame gave me the ability to present the split VC without its frame being sometimes incorrect. And as I noted earlier, by doing this stuff, we're going outside what Apple want us to do, hence the hackery involved.
Ok, I have an idea for what might work: Don't remove the UISplitViewController's view from the view hierarchy. Instead, either put a view on top of it, set the alpha property of its view to 0 or set the hidden property of its view to YES.

How do I remove a view controller from my view hierarchy?

My app has a welcome screen that can only be shown in portrait mode. After the user has tapped through the welcome screen I'd like to show another screen that can be used in both portrait mode and landscape.
I have set up a view controller that implements shouldAutorotateToInterfaceOrientation: returning YES only for UIInterfaceOrientationPortrait, and I add the view to the window with [window addSubView:view]. I tag this view with the tag 1.
When the user taps through the welcome view and the app moves on to the new view I do:
[[window viewWithTag:1] removeFromSuperView];
[window addSubView:myViewController.view];
Where myViewController is an instance of the 2nd view's view controller (that handles the shouldAutorotateToInterfceOrientation method properly).
Now when I rotate, it still calls shouldAutorotateToInterfceOrientation on the original view's view controller, and does not call it on the new view's view controller.
This note from Apple says that only one view controller will get rotation notifications; however, I have removed the other view controller.
What am I doing wrong?
actually that note doesn't say that "only one view controller will get the notifications" but instead it says that "Only the first view controller added to UIWindow will rotate.".
So this might be the problem.
To resolve it, i would say to always have a view added to your window (call it permanent), and add your welcome screen and the next views to this permanent view.
Hope this helps.
as the note you link to state:
Only the first view controller added
to UIWindow will rotate.
So put a flag that makes sure that shouldAutorotateToInterfceOrientation returns NO until the user have dismissed the screen - and then returns YES afterwards. This is a simple and working solution - however, from a code readability point it might be confusing that a "dismissed" view actually controll the rotation.
Personally; my experience is that it's not really worthwhile having some views rotating and some don't - and users tend to don't like it.
happy coding
I wrote up a quick test that shows what you are trying to do should work. Here are the basics I did:
Create two view controllers. App starts with the first view controller being set in the AppDelegate into an instance variable viewController through NIB files. It is then added to the window as you have written.
I then setup an action that when called (could be a Timer, button on first view controller, etc.) that performed the following:
Remove view using [self.viewController removeFromSuperview]. This is different than the way you have done with the tag.
Created second view controller and assigned it to self.viewController.
Added to window like you have specified.
Not sure what is incorrect with your code. I would guess that perhaps the first view wasn't really being removed.

How would you implement [UIViewController presentModalViewController]?

I have to customize the transition between 2 different UIViewController and for that I would like to re-implement [UIViewController presentModalViewController...]. What is inside this function? How does it work? What does it do?
Thanks a lot,
Vance
The -[UIViewController presentModalViewController:animated:] do allot of magic behind the scenes.
For my app Tweet Note I wanted to present modal view controllers by animating the new view from the backside of a view in a shelf. More or less what iBooks does. I tried to override the default implementation but ended up doing it in a multi step solution that works without ugly hacks and caveats:
Instantiate the new view controller.
Apply the transform to the new view controller's managed view, so that it starts in it's initial location.ยจ
Start animating the view into untransformed "normal" position.
Wait for animation to finish, and do as a unit:
Remove the view you just animated from screen.
Call [self presentModalViewController:vc animated:NO].
That last step is important! After your transition animation completes present the modal view controller as normal but without animation, and will appear to the user just as if you did a proper animated presentation.
Dismissing with your custom transition is more or less doing the same thing but in reverse order.

UIViewController vs. UIView - which one should create subviews?

I'm trying to wrap my head around the roles of UIViews and UIViewControllers. If I'm creating and inserting subviews programmatically, is it typical to do this from the view or the controller?
I'm really just wondering if there's a convention/pattern for this. In my sample application, I'm loading 50 images at runtime, adding them as subviews to the main view, then letting the user drag them around the screen. I'm currently doing all the initialization in the view's initWithCoder:
- (id)initWithCoder:(NSCoder*)coder
{
if (self = [super initWithCoder:coder]) {
// load UIImageViews and add them to the subview
}
return self;
}
The view also implements touchesBegan/touchesMoved to allow dragging. My problem comes when I try to access [self frame].size in initWithCoder, it doesn't seem to be initialized yet. This makes me think I may be loading the images in the wrong place...
It depends on what you're doing. If the view represents something "packaged", then you create subviews from the view itself. If you're merely aggregating views together, then you should do it from the view controller.
Think about traditional encapsulation. Is your subview conceptually "part" of its superview, or does it just live there in the drawing hierarchy?
Suppose you're creating a view that contains some controls, like to edit a record with multiple fields or something. Since you're going to be using those text fields and their data in the view controller, they conceptually "belong" to the view controller, and the parent view exists just for grouping and layout purposes. This is probably the more common scenario.
If you were creating a view subclass to behave like a new control, for example, then I would say you should create and manage the views entirely within the view class. I imagine this case will happen less frequently in practice.
It sounds like you may be adding them in the wrong place. If you moved the addition of the subviews into the ViewController, then you could do this work on viewDidLoad and you'd be guaranteed that the main view was already initialized and ready to access.
Dummy explanation would be like this:
Subclass UIViewController whenever you plan to display the view as modal (presentModalViewController) or inside UINavigationController (pushViewController)
Subclass UIView whenever it is a small component inside main view... for example you wish to do custom button or ...
More specifically in your example ... if you are initializing UIViewControllers you can set view's properties eg. like frame size only after your view is added to superview. Only
at that point UIViewController loads and initializes its view.
I think in your case you should use UIViews ... you can set frame size immediately after allocating&initializing them.