How to programmatically launch/dispose of views for the iPhone? - iphone

I am not using a tab bar or any other control -- I want to basically change the current view to a different view controller dynamically (i.e. via code). I'm not using Interface Builder at all...
For example, let's say we create three view controllers:
(this may not be the best example, but I'm trying to make it simplistic)
View_Hello.m (and .h)
View_Goodbye.m (and .h)
View_Ciao.m (and .h)
Our ViewerAppDelegate would launch View01_Hello.
View_Hello would have a custom touch method that would then need to go to View_Ciao if swiped, but View_Goodbye if just touched.
Any ideas on how I could do this (and please don't say "oh, you need to use xxx interface element for this example". I need to be able to randomly change views based on programatic control within the view of the application I'm working on)
I'm been surfing through Google and StackOverflow for the past week, and gone through my three O'Reilly Cocoa books (plus three iPhone developer books), and all of them just use simple interfaces -- but nothing shows an example like what I'm trying to do.
===========
Edit (#Andrew Grant):
for example:
View_Ciao *viewCiao;
-(void) viewDidLoad {
viewCiao = [[View_Ciao alloc] initWithNibName:#"View_Ciao" bundle:nil];
[viewCiao.view setNeedsDisplay];
[super viewDidLoad];
}
This crashes. :-)
=============
Edit (#Daniel Dickison)
Brilliant -- that worked!
- (void)applicationDidFinishLaunching:(UIApplication *)application {
[window addSubview:viewController.view];
[window makeKeyAndVisible];
// create the second view, remove the first, and then display the second
viewCiao = [[ViewCiao alloc] init];
[viewController.view removeFromSuperview];
[window addSubview:viewCiao.view];
}

Have you seen recipe 2-8 "Swiping Views" in Erica Sadun's iPhone Developer's Cookbook? She uses touchesBegan and touchesMoved to determine swipe direction, then sets the animation based on the direction in touchesEnded.
You could extend or amend this to add tap detection in order to determine which view to transition to. The recipe is on or around page 69 if you'd like to have a look, anyway. There's also a video (mov) sample here and a code sample here.

How about something like:
#interface AppDelegate : ...
{
View_Hello *hello;
View_Goodbye *goodbye;
View_Ciao *ciao;
UIViewController *currentView;
UIWindow *window;
}
... IBOutlet properties for the 3 controllers.
#end
#implementation AppDelegate
- (void)switchToGoodbye
{
[currentView.view removeFromSuperview];
[window addSubview:goodbye.view];
currentView = goodbye;
}
... etc.
#end
You may need to resize the view before adding it to the window, and call viewWillDisappear:, viewDidDisappear:, viewWillAppear: and viewDidAppear: on currentView and the new view controller before and after they're removed/added.

Load the appropriate view and add it as a subview to the parent, then remove the existing view from its parent.
If you only have one level of views then your window is the parent.

Related

Question about the mechanics of iPhone view controllers (i.e., explain why this crashes)

I am pretty new to iPhone programming, and was playing around with an app yesterday trying different scenarios with view controllers and nib files. So, I started a new app with a FirstViewController (FVC for short) and an FVC.xib.
I layed out a quick view in FVC.xib and ran the app - view displays, great.
I now wanted to have a second view I could add on top of the main view. So I went ahead and created SecondViewController.xib (SVC) but did not create the .m and .h files. I went about trying to load both these views from the same view controller, and here is where my question lies:
I created a button in FVC.xib and created an IBAction like this:
- (IBAction)loadSVC {
FirstViewController *viewController = [[FirstViewController alloc] initWithNibName:#"SecondViewController" bundle:[NSBundle mainBundle]];
secondView = viewcontroller.view;
[viewController release];
[self.view addSubView:secondView];
}
So this works great and adds the contents of SVC.xib, but when I try and remove that view from the superview, the app crashes:
[secondView removeFromSuperview];
If I actually create a view controller for SVC, use that to instantiate my view in FVC, and move the remove code to the SVC:
[self.view removeFromSuperview];
Everything works. My question - I kind of get why my first method crashes, but I was hoping someone could explain why and what goes on behind the scenes. I'm still a noob with object oriented programming, so what is actually happening in my first case where I create a new instance of FirstViewController and add its view to self.view? Why can't I release it (I assume because the original view is associated with FirstViewController, and when I create a new instance with the second xib it messes everything up) - I'd love a more technical explanation as to what is happening...
Thanks much!!
EDIT to add more info in response to Nick's reply below
Nick - so your answer did clear my thinking a bit in regards to the retain count, etc... I did another test app trying to get this working from a single view controller - think, for example, that I wanted to display an Alert or Welcome message to the user (I know in a real app there are different methods to accomplish this, but this is more of a learning experience) -- so I have my main view # MainViewController and layout my alert message in a xib called alert.xib -- so there is no logic behind the alert message, no reason for it to have a view controller that I can see, my end goal being loading/unloading this on top of my main view from the main view's view controller (or understanding why it is impossible)
I tried this using instance variables as you recommended:
In MainViewController.h:
#import <UIKit/UIKit.h>
UIViewController *secondController;
UIView *secondView;
#interface MainViewController : UIViewController {
}
#property(nonatomic, retain) UIViewController *secondController;
#property(nonatomic, retain) UIView *secondView;
- (IBAction)loadSecond;
- (IBAction)removeSecond;
#end
In MainViewController.m:
#import "MainViewController.h"
#implementation MainViewController
#synthesize secondController, secondView;
- (IBAction)loadSecond {
secondController = [[MainViewController alloc] initWithNibName:#"alert" bundle:[NSBundle mainBundle]];
secondView = secondController.view;
[self.view addSubview:secondView];
}
- (IBAction)removeSecond {
//I've tried a number of things here, like [secondView removeFromSuperview];, [self.secondView removeFromSuperview];, [secondController.view removeFromSuperview];
}
- (void)dealloc {
[secondController release];
[secondView release];
[super dealloc];
}
So - this works to load the alert view, but the removeSecond button does nothing (I did use NSLog to verify the removeSecond method is fired) - why?
Second, and most importantly - is this even possible, or is it horrible practice? Should every nib/view I am manipulating have their own view controller? Am I wrong to think I could just make a new instance of MainViewController and use it to display and remove this no-functionality, very temporary view? (And yes, I realize I could easily create this view programatically or accomplish the end goal in many different ways which would be easier, but I'm trying to really learn this stuff and I think figuring this out will help...
Thanks for the help!
You created a view controller
You accessed its view which caused controller to create the view and call the delegates (i.e. viewDidLoad)
Controller returns the view that you asked for
Now you add the view as a subview which increases its retain count
Controller is released and it releases the view, BUT since view's retain count was increased the view is still there
You try to remove the view, it is unloaded and delegates are to be called (e.g. viewDidUnload), however that messes up since the controller who created the view is released and that piece of memory is... smth else :)
That's why the first method doesn't work.
The second method is NOT correct either but it works because:
You remove controller's view from superview but since controller itself is not released (you didn't call [self release] or anything like that, not saying that you should :), just an example), then the view didn't reach 0 (zero) retain count and is still there - which means its subviews aren't removed
The proper way to do it is to save the reference to the controller as an instance variable (usually declare a synthesized property), and release it only when you are done with the view, making sure that the view is removed from superview before hand. The default templete for a View Based App shows how view controller should be managed
Hope this helps to understand why both methods behave differently
Based on your clarifications, you don't need secondView property or iVar. Also in your loadSecond instead of secontController = bla you need self.secondController = bla, otherwise you simply assign reference to the iVar instead of going through the setter.
Yes, it's possible to load subviews/other resources from a nib without having a dedicated controller
This is how you do it (one of the approaches):
UIView *result = nil;
NSArray *bundle = [[NSBundle mainBundle] loadNibNamed:#"MyNibName" owner:owner options:nil];
for ( id o in bundle ) {
if ( [o isKindOfClass:[UIView class]] ) {
result = (UIView *)o;
break;
}
}
Here the result will contain the first UIView in MyNibName. You can use other criteria to find out whether you got the view you wanted (tags, types...)

UIModalTransitionStylePartialCurl with UITabBarController

This question has been asked a lot e.g. here but as far as I can see is yet to be answered in full.
I have a UITabBarController with a UINavigationController as the root vc for one of the tabs, which itself has a MKMapView as its root vc. The behaviour I want is for the map to partially curl upwards, while leaving the tab bar in place (similar to the Maps app).
So far all I have managed to get working is for the whole view to curl, which isn't as nice.
Solutions I have seen are to set the hidesBottomBarWhenPushed property to NO, which would make sense however this doesn't seem to work, (unless I am doing something wrong).
For clarity, my code is as follows:
MyVC *aView = [MyVC init];
aView.modalTransitionStyle = UIModalTransitionStylePartialCurl;
aView.hidesBottomBarWhenPushed = NO;
For the presenting part, I have tried the two alternatives below, neither of which seem to work:
[self presentModalViewController:updateStatus animated:YES];
[[self navigationController] presentModalViewController:updateStatus animated:YES];
Any help much appreciated.
I've scoured StackOverflow (and the Internet) for a solution to this problem. The question has been asked many times, but as you note, never sufficiently answered. Many solutions give an acceptable solution if it is unimportant whether, e.g., a lower toolbar curls up as well.
Others have provided a solution using UIView animations / CoreAnimation rather than UIModalTransitionStylePartialCurl as a modal transition style; this is at worst a solution not allowed in the App Store, and at best is not quite the same effect as one gets from UIModalTransitionStylePartialCurl (e.g. the shape of the curl is different).
None of these solutions have provided an answer that mimics Apple's solution in the Maps app (i.e., using UIModalTransitionStylePartialCurl but leaving an un-curled UIToolbar at the bottom of the screen).
I will continue in this tradition of incomplete answers, since you ask about a UITabBarController and my solution doesn't specifically address that case. It does, however, solve the problem I had, which was to get a half page curl with an un-curled toolbar at the bottom.
There must be a more elegant way to do this, but this is how I managed.
The rootViewController of my AppDelegate is a subclass of UIViewController, which I'll call TAContainerViewController. TAContainerViewController manages a) the actual contents of the screen (the "stuff to be curled"), TAContentViewController, and b) the contents "behind" the TAContentViewController (e.g. settings), which I'll call TAUnderCurlViewController.
My instance of TAContainerViewController had properties for a TAContentViewController and a TAUnderCurlViewController. The UIView that was my content was a subview of TAContentViewController's view property; likewise what the user sees under the curl is the view property of the TAUnderCurlViewController.
In the init method of TAContainerViewController I make sure to do the following:
_underCurlVC.modalTransitionStyle = UIModalTransitionStylePartialCurl;
And to curl the contents to reveal under the page, I set up an action that calls this code:
[self.contentVC presentModalViewController:self.underCurlVC animated:YES];`
where self is the TAContainerViewController, contentVC is an instance of TAContentViewController, and underCurlVC is an instance of TAUnderCurlViewController.
To dismiss the view, simply [self.contentVC dismissModalViewControllerAnimated:YES];.
Some strangeness seems to occur with the frame of contentVC when the modal view is dismissed, so I manually reset the frame when the modal view is dismissed.
I've posted a sample project with more details on Github. Hopefully someone can take this and turn it into a slightly more elegant solution, or expand it to work with a UINavigationController or UITabBarController. I think the trick is to pull the View Controllers out of the well-defined relationships in the Cocoa subclasses, so maybe subclassing those specialty View Controllers would do it.
Tim Arnold's response worked great for me, thanks!
One trap to watch out for: your modal page-curl transition will take over the whole screen if your content view controller is added as a child of the container view controller. You could just not add it as a child, but then none of the view lifecycle methods will get called on your content controller (e.g. viewDidLoad, viewWillAppear), which could be a problem.
Fortunately, there is a way around this. In your container controller:
Add your content controller as a child in viewDidLoad
Remove it as a child in viewDidAppear
Re-add it as a child in viewWillDisappear.
That way, your content controller gets its lifecycle methods called, while still being able to do a modal page-curl transition without taking up the whole screen.
Here is the entire code of a bare-bones solution:
#interface XXContainerController : UIViewController
#property (strong, nonatomic) UIViewController *contentController;
#property (nonatomic) BOOL curled;
#end
#implementation XXContainerController
#synthesize contentController = _contentController;
#synthesize curled = _curled;
- (void)viewDidLoad
{
[super viewDidLoad];
self.contentController = [self.storyboard
instantiateViewControllerWithIdentifier:#"SomeControllerInStoryboard"];
// Add content controller as child view controller.
// This way, it will receive all the view lifecycle events
[self addChildViewController:self.contentController];
self.contentController.view.frame = self.view.bounds;
[self.view addSubview:self.contentController.view];
[self.contentController didMoveToParentViewController:self];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Remove the content controller as child view controller.
// This way, the modal page curl transition will
// not take over the whole screen.
// NOTE: need to wait until content controller has appeared
// (which will happen later).
// Achieve this by running the code at the end of the animation loop
[UIView animateWithDuration:0 animations:nil completion:^(BOOL finished) {
[self.contentController removeFromParentViewController];
}];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// Add the content controller as child view controller again
// so it receives the view lifecycle events
[self addChildViewController:self.contentController];
}
- (void)setCurled:(BOOL)curled
{
if (curled == _curled) return;
_curled = curled;
// Curl up the content view and show underneath controller's view
if (curled) {
// Note you can specify any modal transition in storyboard
// E.g. page curl, flip horizontal
[self.contentController
performSegueWithIdentifier:#"SomeModalSegueDefinedInStoryboard"
sender:self];
// Uncurl and show the content controller's view again
} else {
[self.contentController dismissModalViewControllerAnimated:YES];
// Have to do this, otherwise the content controller's view
// gets messed up for some reason
self.contentController.view.frame = self.view.bounds;
}
}
#end

Same WebView on every view

Basically i have a WebView on SecondViewController and I wish for the WebView to be visible on every view like a tab bar and fully controllable on each view.
Please note the WebView will be on a webpage with a online slideshow so I cannot simply reload on each view
Also in the SecondViewController I have
- (void)webViewDidFinishLoad:(UIWebView *)YouTubePlayer {
I would suggest adding the webView on you window after the you add the tabbarcontroller.view just like:
[window addSubview:tabbarController.view];
[window addSubview:webview];
[window makeKeyAndVisible];
and initially make don't make it visible. You should handle all the webview related methods in the app delegate. Now whenever you don't need it you can hide it by calling the methods your wrote in app delegate from your view controllers.
Hope this helps.
I'd just set up a singleton UIWebView and add it to each view-controller-view when that view controller is about to become visible. Here's one way to do it:
//.h
#interface SharedWebView : UIWebView
{
}
+ (SharedWebView*) shared;
#end
//.m
SharedWebView* g_sharedWebView;
#implementation SharedWebView
+ (SharedWebView*) shared
{
if ( g_sharedWebView == nil )
{
g_sharedWebView = [[SharedWebView alloc] init];
// ... any other intialization you want to do
}
return g_sharedWebView;
}
#end
// in your view controller(s)
#property (readonly) UIWebView* webView
- (UIWebView*) webView
{
return [SharedWebView shared];
}
- (void) viewWillAppear: (BOOL) animated
{
[super viewWillAppear: animated];
[self.view addSubview: self.webView ];
self.webView.frame = CGRectMake(10, 10, 300, 300);
// want to re-set the delegate?
// self.webView.delegate = self;
}
The simplest approach is to just make all of your view controllers aware of this extra view (make the view available through a singleton PinnedViewController or whatever). During each view controller's -viewWillAppear, just do this:
[self addSubview:[[PinnedViewController sharedController] view]];
This will move the view to whoever is currently active (making a view your subview automatically removes you from your old hierarchy).
If that is cumbersome or otherwise unworkable, there are two other options. First, you can subclass UITabViewController (I assume that's what you're using here from your question), and inject your extra view (resizing the content view to make room). This is undocumented and unsupported, so take heed. But it's not incredibly difficult if you don't do too many other fancy tricks.
The other tricky solution is to create a second UIWindow that you float over the main UIWindow (or resize the main UIWindow to make room for it). This is only semi-documented and is also not really supported. But this approach can work if you're trying to put the extra view below the tabbar for instance.
But if your system is simple enough, I recommend just letting your view controllers all manage the pinned view manually. You'll save a lot of code spelunking that way, and you won't have to rely on any undocumented internal view hierarchies.
Sounds like you try to put views on top of this view but not modal. There was this blog entry I once saw that described how you would do something like this. I think it should apply also for your case: semi-modal-transparent-dialogs-on-the-iphone
In iOS UIViewControllers are expected to manage an entire "screen" worth of content so it's not normal to try to share a single view across many view controllers. Trying to have UIViewControllers whose views only manage part of their window is problematic and will result in unexpected behavior as UIKit will not send messages like -viewWillAppear to all view controllers with visible views. Instead you would normally create a single UIViewController whose view includes that web view and whatever other views compose your tab like interface.
Alternately you could have a hierarchy of many view controllers and add a single web view as a subview of all of them. You would then pull your web view delegate behavior out into some non-UIViewController controller class to manage the behavior of the web view.
You can have all your views take up a portion of the screen and have your UIWebView take up the rest. The logic for switching between the other views should remain the same.
For example, in your viewDidLoad method for your UIViewControllers, you could have something like:
self.view.frame = CGRectMake(0, 100, 320, 380);
And in your (say) AppDelegate, you would have your normal call to show the main UIViewController (in your case, it sounds like a UITabBarController?) and also have a call to add the UIWebView. Say, something like:
myWebView.view.frame = CGRectMake(0, 0, 320, 100);
The view controllers and UIWebView would be independent of each other.
You see this pattern, I believe, with apps that have iAds. I've done something just like this with one of my free apps.
Hope this helps!
I got the same effect by simply adding it to the navigationController itself(if you don't have one then just add it).
works great for me in one of my apps.
Could you not use two webviews on your application and simply change the uppermost webview with your more dynamic content?

Multiple view controllers with their own xibs, and when they load

Going through many tutorials and with the help of everyone here, I am trying to wrap my head around using multi view controllers with their own xib files.
I have one example where there is a : multiViewController and two others: aboutViewController, rulesViewController.
In both aboutViewController.m and rulesViewController.m files, I have placed the following code:
- (void)viewDidLoad {
NSLog(#"rules View did load"); // (Or About View did load, depending on the .m file)
[super viewDidLoad];
}
The mainViewController.m file contains:
-(IBAction) loadRules:(id) sender {
[self clearView];
[self.view insertSubview:rulesViewController.view atIndex:0];
}
-(IBAction) loadAbout:(id) sender {
[self clearView];
[self.view insertSubview:aboutViewController.view atIndex:0];
}
My question is, why is it when I run the application does the ViewDidLoad for both About and Rules fire? Meaning, I get the NSLog messages. Does this mean that regardless of the separate xib files, all views are loaded on start up?
My point of all this is: I want the multiViewController to be my main menu which will have separate buttons for displaying the other xib files when clicked. I had been placing my "initalize" code in the viewDidLoad but this seems wrong now as I don't want to hit until the users presses the button to display that view.
An example was to have a view that is: playGameViewController. In the viewDidLoad I was checking to see if a prior game was in progress and if so, prompt the user if they would like to resume. In the above case, when the app starts, it prompts right away (because the viewDidLoad is firing) even though I only wanted to display the main menu first.
Any explanation is greatly appreciated. Thanks in advance.
Geo...
My question is, why is it when I run
the application does the ViewDidLoad
for both About and Rules fire?
Meaning, I get the NSLog messages.
Does this mean that regardless of the
separate xib files, all views are
loaded on start up?
When you call
[self.view insertSubview:rulesViewController.view atIndex:0];
it's going to first call viewDidLoad for the initial view and then viewDidLoad once again for RulesViewController.
When your MainViewController, or any view for that matter loads, viewDidLoad is called automatically. ViewDidLoad is there in order for you to override or modify any objects in the nib, or you can create objects yourself. Views are only loaded on an as needed basis. If you were to load all your views initially when the app boots up, the user would just see a black screen until all the views are processed.
You say you are going through some tutorials, I don't know your area of expertise yet, but have you looked into navigation based apps using UINavigationController?
Just an example as your request if you want to have a button load a view you can do something like.
- (IBAction)pushSecondView:(id)sender {
SecondViewController *secondView = [[SecondViewController alloc] initWithNibName:#"SecondViewController" bundle:nil];
if (secondView != nil) {
secondView.title = #"Second View";
[self.navigationController pushViewController: secondView animated: YES];
}
}

UIViewController.... AppDelegate.... Where does the code go!

I'm slowly picking up Objective-C and the iPhoneSDK but I'm having some problems getting my head around the MVC pattern.
I'm fleshing out a game which I hope will have screens like a splash screen, title, help etc. What I'm currently doing is creating a new UIViewController and a new nib for each of these screens, is this the right practice? In the main AppDelegate I've created methods that show the views and add them with [window addSubView:controller.view]. What I'm finding is that with the show/hide code sat in the AppDelegate, I have to create a reference of the AppDelegate in the loaded controller in order to target the hide code.
This seems a bit awkward but I expect I'm probably approaching this wrong, how do you guys usually do this sort of thing?
// example from AppDelegate
-(IBAction)showHelp:(id)sender
{
helpScreen = [[helpController alloc] initWithNibName:#"helpView" bundle:nil];
// send copy of self in order to target closeHelp method from InterfaceBuilder
helpScreen.appDel = self;
helpScreen.view.alpha = 0;
[window addSubview:helpScreen.view];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:1.0];
helpScreen.view.alpha = 1.0;
[UIView commitAnimations];
}
Many Thanks,
UIViewController and a new nib for each of these screens, is this the right practice
Yeap!
[window addSubView:controller.view]
Are you also removing the old views at the end of the animation? You should be, otherwise you would have multiple view controllers running at once, something that you really don't want.
What I'm finding is that with the
show/hide code sat in the AppDelegate,
I have to create a reference of the
AppDelegate in the loaded controller
in order to target the hide code
Well somewhere you need the code that's responsible for switching views, and if the views can control this switching then they do need a way to trigger that. Rather than app delegate I usually have a RootViewController that performs these changes.
I tend to derive each of these views from a base class that has a delegate property for performing these changes. When the views need to change they call functions in the delegate. These are usually;
pushView - temporarily pushes the view as active, current view is removed from the view hierarchy but not destroyed. This would be used for something like a help screen.
popView - current view is destroyed and the previous view is reinstated. This is how the help screen would remove itself.
changeView - current view is destroyed and replaced with the specified view. This might be how you change from page1 to page2 of the help.
E.g.
// your root controller
-(void) changeView:(UIViewController) newController
{
newController = blah blah;
newController.delegate = self;
// add newController view, remove old one etc
}
// new controller
-(void) userPressedHelp
{
UIViewController* help = blah blah;
[self.delegate pushView: newController];
}
// help controller
-(void) userPressedOk
{
[self.delegate popView];
}
That seems reasonable to me. I'm pretty new to Obj-C but that's how I've done an application.
As long as the ViewControllers don't have knowledge of each other, I think you are doing just fine.