Can you cache UIViewControllers? - iphone

I have an application with 8 UIViewControllers presented by navigating from left/right/up/down. They are created on start of the app and kept in an NSArray. Only 1 of them is added to the view tree (addSubview:) at any time - the rest just sit in the cache until they are needed.
Now the problem I am having is when rotating to Landscape. Only the currently visible view changes the bounds.size to Landscape. When navigating to the next view, that view still thinks it is in Portrait (but the containing views will all look in Landscape).
By the way, the view hierarchy of the app is the following: UIWindow -> Main UIViewController -> 1 of the 8 cached UIViewControllers.
For example:
* Change orientation:
- Main UIViewController.view.bounds.size = 480x300 (OK)
- One of the cached UIViewControllers view.bounds.size = 480x300 (OK)
* Go to next view:
- Main UIViewController.view.bounds.size = 480x300 (OK),
- Another of the cached UIViewControllers view.bounds.size = 320x460 (??)
Not sure whats going on. Do I have to tell the cached UIViewControllers somehow that the orientation/size changed or something else?
Thanks a lot

Yes you can. Optimally you should purge the view that each view controller manages when it goes off screen.
Simply assign nil to the view property of the view controller that is being replaced. This will free all resourced used by the view that is not visible anyway. As an added bonus the view is then recreated with proper frame whenever you decide to brin a particular viw controller in front again.
I am also assuming that what you are implementing is a subclass of UIViewController that acts as a container for other view controllers. Like a sibling to UITabController named CWGridController or similar. Noe that it is the responsibility of the the parent view controller (your subclass) to size the frame of it's child view controllers views as needed.
Creating a container view controller is not a small task, and the support for doing it is not complete from Apple. There are a few things you must do including but not limited to:
Forward all orientation change calls to all child view controllers.
Properly call all appear/disappear on child view controller as needed.
The last part to make it work is to break some rules. View controllers works very badly unless they know about their parent view controller, layout will be wrong and modal view controller behaves strange. Unfortunately the parentViewController is readonly, or so you would think. Setting it anyway using KVC will solve most problems, and Apple does not seem to object when you submit to App Store:
[childViewController setValue:self forKey:#"parentViewController"];

In viewWillAppear you will have to check the interface orientation and set the frames accordingly.

Related

Why is it incorrect to insert the view from one view controller into another?

In an iPad application I have a tab Controller containing several view controllers. One of these view Controller (call it MainViewController) needs 2 table views side by side.
So I wrote 2 UITableViewController subclasses and from MainViewController, I alloc/init both subclasses of UITableViewController, and add the tableview from each to the MainViewController's view.
This means that UITableViewController subclasses's views are subviews of MainViewController's view.
This answer: https://stackoverflow.com/a/7684648/191463 says that doing that is incorrect and it seems Apple are starting to cut down on it.
I really do not want to have to put all the code from both UITableViewControllers in MainViewController, as it will make it much harder to read and in future could create duplicate code, if I want to use one of the tableview elsewhere in the app by itself.
Is this actually a problem, if it is how do I do it properly?
Apple isn't cutting down on it. This is the only way to create custom container view controllers prior to iOS 5. Apple actually listened to the developers and made it easier to do this sort of thing in iOS 5 with child and parent view controller methods, not to mention they made it so it worked hierarchically.
In most cases, this wouldn't actually be a problem in terms of applications crashing or performance or anything. It can be a problem in some cases, because let's say you have a child view controller. You add the view controller's view to your root view controller. Prior to iOS 5, child view controllers were things like navigation controller view controllers, tab bar controller view controllers, and modal view controllers. What happens when you have a button that calls [self.parentViewController dismissModalViewControllerAnimated:YES];? Technically, the view controller isn't being presented as a modal view controller, you added the view to the root view controller view.
In iOS5, you're able to add child view controllers to view controllers and are able to transition from one child view controller to another.
Now even if your view controller doesn't have a different parent, adding a "root" view controller to another root view controller isn't the best way to do it (especially since you don't get access to the parent view controller unless you explicitly create a parentViewController pointer in the child view controller). So in the end, Apple just made it easier and more decoupled.
It is OK to do it so long you take the responsibility of managing the viewController life cycle events
initWithNibName...
loadView:
viewDidLoad:...
.
.
viewDidUnload..
dealloc
memoryWarnings
orientation changes
So if you create a custom "container view controller" it becomes your responsibility to call all these methods on child viewControllers at the appropriate time. Think of it as "If you were to implement UITabBarController" what all will you have to manage regarding the children ??"
It quickly gets complex. Adding another viewController's view as subview is childs play.
iOS 5 does some of this stuff for you by specifying parent child relationship, however I still haven't seen any sample code anywhere yet to point to.
I'd say it is not incorrect or wrong to create view controller containers, especially when Apple engineers do that themselves. UITabBarController, UINavigationController or UISplitViewController - they are all view controller containers. More over many great apps with unique UX do that more common than you think. However the real issue is that it's quite hard to do it the right way, so e.g. view lifecycle, memory management and rotation handling is done properly along the hierarchy of views. Fortunately Apple guys did a decent job and iOS5 introduced lots of functionalities regarding controller containers:
UIViewController class reference
View Controller Programming Guide for iOS
If you're interested how above problems had to be addressed before iOS5, read these two very good blog posts:
Writing high-quality view controller containers
Using view controllers wisely

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.

How does iOS know which view controller should be active?

If I want to replace one screen of an app with another, but I don't use a navbar/tabbar controller, then I could just remove oldViewController.view from window and add newViewController.view to it. That's all, now newViewController will get rotation events, etc.
But UIView does not reference "its" controller, so how is this possible, how iOS know it should make newViewController an active one? Does iOS do some magic, it internally references controller from view or what?
UPDATE:
I think I was misunderstood: I don't ask how to make some view controller an active one - I know that. I'm just curious, how is it possible that I pass some view to UIWindow object ([window addSubview:view]) and it somehow finds view controller although view doesn't know its controller.
yeh I had the same question like you. and I figured it out.
UIView is derived from UIResponder. and UIView must subclass UIResponder::nextResponder.
Its default implementation is returning a view controller of the view (if it hadn't, it would be super view)
So, consequently view can see its controller. that means window know the topmost view and also
its controller.
good luck.
Unfortunately, iOS only send events to the first ViewController of the stack. You can try and present a new one on the top of others with video for example, it will never rotate.
If you don't use navbar/tabbar controller you will have to add and remove everytime from the Window to keep only one at the time if you wand to have events.
The main UIWindow class for your application will have a view controller set in its rootViewController property. That controller's view is the "main" view for the app. This is usually setup in the main .xib for the project. That view controller will receive the usual events like "viewDidAppear" or "willRotateToInterfaceOrientation". You can put up your own view over top of it if you want to, but you will need to manage those events yourself. Usually you don't do that though. You just use a UINavigationController or UITabBarController as your rootViewController and allow them to manage getting the events to new "pushed" view controllers, or you popup view controllers with "presentModalViewController".

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 to tell if view has appeared via popping or not?

Using a UINavigationViewController, how do I find out how a view has appeared?
The view has either appeared in a straightforward manner, as the first view in the UINavigationController stack. Or it has appeared because a second view has been popped and the first view has revealed itself again. How do you find out which of these happened?
The only reliable way to do this, as far as I'm aware, is to subclass UINavigationController and override the UINavigationBarDelegate methods:
– navigationBar:shouldPushItem:
– navigationBar:didPushItem:
– navigationBar:shouldPopItem:
– navigationBar:didPopItem:
Don't forget to call super, of course.
Simple approach is to add a property to your RootViewController to track whether or not it has pushed another view onto the navigationController.
-(BOOL)hasPushedSecondView;
Initialize to NO in your init method.
Before pushing secondViewControllers view onto the stack, update the property to YES.
In viewWillAppear, check the value and update your view accordingly. Depending on how you want the application to behave you may need to reset the hasPushedsecondview property back to NO.
you could take a look at the leftBarButtonItem or backBarButtonItem, based on how your application is written and determine how the view appeared. If it is on top, unless you have a custom leftBarButtonItem, there would be no object there.
You can determine this directly via a couple of methods on your UIViewController subclass.
From Apple's documentation:
Occasionally, it can be useful to know why a view is appearing or
disappearing. For example, you might want to know whether a view
appeared because it was just added to a container or whether it
appeared because some other content that obscured it was removed. This
particular example often appears when using navigation controllers;
your content controller’s view may appear because the view controller
was just pushed onto the navigation stack or it might appear because
controllers previously above it were popped from the stack.
The UIViewController class provides methods your view controller can
call to determine why the appearance change occurred.
isMovingFromParentViewController: view was hidden because view controller was removed from container
isMovingToParentViewController: view is shown because it's being added to a container
isBeingPresented: view is being shown because it was presented by another view controller
isBeingDismissed: view is being hidden because it was just dimissed