Objective-C Manage Several View Controllers - objective-c

I'm new to Objective-C, and I'm looking for some advice on how to manage multiple view controllers.
I've looked through Apple's documentation on their built-in container view controller classes, and none of them seem to be what I'm looking for -- the closest is NavigationController, but even that seems a little bit off.
I want to implement a series of ViewControllers -- which use xibs for their interfaces -- that transition from one to the next according to a series of rules. For example, on app load, we see if we have a userId in local storage -- if we don't, show the signup screen. Next, there's a button to (say) order a taxi -- if that button is clicked, show the confirm screen.
Optional Aside: The reason I don't think this fits the Navigation controller is that the flow doesn't seem hierarchical, but rather kind of branchy and linear. One concrete example of this is that I don't need a navigation bar to go back, which seems to come standard on the Navigation Controller. But I don't know the NavigationController well enough to know for sure whether or not it fits this usecase.
I've been hacking this with a variety of methods. For example, in an IBAction handler, I've been using this code to transition to a new view controller:
UIViewController *view = [[UIViewController alloc] initWithNibName:#"CCWConfirmViewController" bundle:nil];
view.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentViewController:view animated:YES completion:nil];
Also, in my window's 'Root' ViewController (which I set to be the SignupViewController) initWithNibName, I return a different view controller than the one asked for, depending on the result of the local storage call I mentioned earlier:
if (currentUser.userId) {
// Instead of returning the SignupViewController, like was asked,
// return the MainViewController, since signup isn't needed for existing
// users.
CCWMainViewController *mvc = [[CCWMainViewController alloc] init];
return mvc;
I seem like I have to be doing something wrong (the second hack builds but generates a warning, since I'm returning a pointer to the wrong type). Anyone know a better way? Is the NavigationController for me after all, and I'm just misinterpreting its purpose? Do I just need to implement a custom container to serve as my RootViewController and manage these other ViewControllers?

Your decision is right. You'll not need a navigation controller for your purpose, but as they say.. There are a lot of ways by which you can achieve a result.
"I don't need a navigation bar to go back, which seems to come standard on the Navigation Controller"
You can always hide the navigation bar using self.navigationController.navigationBarHidden = YES
Coming back to the point, I would not say what you have done is wrong but would propose a better approach which involves the concept of view containment.
In cocoa touch you can add any view controller as a child view controller. So here's what I propose.
Create a class called RootViewController which will always be created and set to your window regardless of the condition the user is logged in or not. In the viewDidLoad of this class
-(void)viewDidLoad
{
if (currentUser.userId) {
CCWMainViewController *mvc = [[CCWMainViewController alloc] init];
[self addChildViewController:mvc];
mvc.view.frame = self.view.bounds;
[self.view addSubview:mvc.view];
}
else{
//Create signup/login view and add to view as above.
}
}

Related

view controllers: presentation, dismissal

Just for the purpose of learning some particular aspects of xCode, I am creating a simple app that has 2 functional view controllers. Each contains a button that can be pressed to switch to the other. I am not using segues. I am using pointers retrieved from the app delegate.
visual illustration (click for higher resolution):
When the app loads, the root view controller presents view 1. When you click "switch to view 2," the following code causes view 2 to appear:
- (IBAction)buttonPressed:(id)sender
{
AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
[self presentViewController:appDelegate.view2 animated:YES completion:nil];
}
So far, so good.
But when you click "switch to view 1" on the second view, this same code (replacing "view2" with "view1") gives the following error:
Application tried to present modally an active controller.
So to summarize (where --> = presents), we have root --> view1 --> view2 -x-> view1
I don't care about retaining the history of who presents whom. I simply want the buttons to bring to the top (make visible) a previously displayed view controller, maintaining the state of its views.
It would be nice to know the following:
Is there a workaround that would enable me to achieve the intended behavior using presentViewController? E.g., root --> view2 --> view1
What other method(s) would be more practical for achieving the desired behavior? It/they must use the app delegate because in my real application that will be unavoidable.
Am I breaking the rules by trying to put a view controller on top without integrating into some larger architecture? E.g, is this sort of behavior supposed to be handled by navigation conrollers and pushing/popping? If so, can you explain why xCode doesn't want me to do this? Why can't I just display whatever view controller I want, without it necessarily having any relationship to other view controllers? (Maybe because that could lead to abuse of the app delegate?)
What does it really mean to "present" a view controller? What functional limitations or capabilities does it entail beyond creating pointers between presenting and presenter? What is the importance of leaving the presenting view controller "active"?
If instead make the button on view1 send the presentViewController message to the root view (which I hoped would just change the presentation chain from root --> view1 to root --> view2, leaving view1 still existing in memory but not part of this chain), I get a different error: "Attempt to present on whose view is not in the window hierarchy!" What does this mean? I can't find an explanation of window hierarchy.
Okay, I know I'm asking a lot here, but any amount of enlightenment will be greatly appreciated!!
The correct way to do this is to get the underlying rootVC to do the presenting and dismissing (as you attempt - without the dismissing part - in point 5). You can achieve this by sending a message + completion block back to the rootVC from each of view1 and view2 when you want to present the other.
When you are in view1:
- (IBAction)buttonPressed:(id)sender
AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
UIViewController* presentingVC = self.presentingViewController;
[presentingVC dismissViewControllerAnimated:YES completion:^{
[presentingVC presentViewController:appDelegate.view2
animated:YES
completion:nil];
}];
}
and similarly for view2. Take care that you need this line:
UIViewController* presentingVC = self.presentingViewController;
as you can't refer to 'self.presentingViewController' inside the completion block as it's controller has been dismissed at this point.
I think that answers points 1 and 2.
To answer point 3 "Why can't I just display whatever view controller I want, without it necessarily having any relationship to other view controllers?" - well you can (via the rootViewController property of the window), but then you are going to have to implement navigation and manage your viewController pointers, which means you will end up creating a controller of some sort. Apple is helping you here by providing you with a few which cover most needs.
As regards your point 4 - the presenting of a viewController is controlled by the presenting VC, which is why you want to keep that one 'active'. When you send this message:
[self dismissViewControllerAnimated:completion:], self just reroutes the messge to it's presentingViewController. If you get rid of your presentingViewController your dismiss method will break.
Point 5 is answered above. You need to dismiss the topmost view first before asking an underlying view to present. Note that view1 is "still in memory" but only because you have retained a pointer to it in your app delegate.
update
As you are trying to get this to work with an initial launch-straight-to-view1, you could make a BOOL launched property and check/set it from your rootViewController's viewDidAppear:
- (void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (!self.launched) {
self.launched = TRUE;
AppDelegate *appDelegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
[self presentViewController:appDelegate.view1
animated:YES
completion:nil];
}
}
Let me try to tackle your points one by one.
1) No, you shouldn't do this all with presentViewController.
2) If you want to do root --> view1 --> view2 --> view1, then you don't do that all with presentViewController. To go from view1 back to view2 you should use dismissViewControllerAnimated:completion.
3) The view controllers do have a relationship when you use presentViewController:animated:. The presenting controller has a pointer to the one it presents, and the presented one has a pointer to the one that presented it. So, you're getting these relationships whether you want it or not. There is a way to display whatever controller you want with no relationship between them -- just reset the window's root view controller. The old view controller will be deallocated (if you don't keep a strong pointer to it), and the new one becomes the window's root view controller.
4) Presenting a view controller makes that controller a modal view controller -- it takes over the whole screen and is intended to be used as an interruption in the flow of the app. You really shouldn't use them extensively to go from one controller to another (and especially not for going "backwards" to previous controllers). Because of the way it's supposed to be used, you normally want to go back to the controller that presented it, so that's why it's kept "active" (in the sense that it's not deallocated).
5) You get that error because root's view is not on screen, view1's is. You need to present a view controller from the controller on screen.

Does dismissViewControllerAnimated remove the controller's instance

For my app, I want to have a few different instances of the same view controller. For now, I am just creating a new instance like this:
iSafeViewController *tab = [[iSafeViewController alloc] init];
[tab setModalPresentationStyle:UIModalPresentationFullScreen];
[tab setModalTransitionStyle:UIModalTransitionStyleCrossDissolve];
[self presentViewController:tab animated:YES completion:nil];
Great. And since this is done in the iSafeViewController class anyway, I have another button that currently just dismisses the latest controller on the stack.
- (IBAction)closeTab:(id)sender {
[self dismissViewControllerAnimated:YES completion:nil];
}
Okay, however, I really want to be able to go back to these instances. So, I have two questions.
Does dismissViewControllerAnimated remove that controller's instance from memory. If not, is there a way I can re-present it.
There is probably a better way to navigate through viewController instances then presentViewControllerAnimated. At the very least, is there a better way to create new instances of one's viewController and be able to navigate to each of them, hopefully not in a stack. In other words, if there are three viewController instances, is there a way I can go from the third to the main one?
Any ideas would be appreciated. Thanks.
"Does dismissViewControllerAnimated remove that controller's instance from memory? If not, is there a way I can re-present it."
Calling dismissViewControllerAnimated does not explicitly remove a view controller from memory, but if no other part of the code is storing a strong reference to the view controller, once the presenting view controller dismisses your VC, it may be deallocated as per the normal memory management system.
So if you ensure something in your code has a reference to your view controller (aside from the VC which is presenting it modally), it won't disappear after being dismissed, and yes this means you can re-use it.
As for "random access" to view controllers: you could use UINavigationController and use methods like popToViewController:animated: and multiple calls to pushViewController:animated: (without animation!) to create the effect of travelling to arbitrary view controllers. This feels like a bit of a hack.
Alternatively, and preferably, you could write your own custom container view controller. This is a view controller that deals with presenting other view controllers. See Apple docs.
Here's a good WWDC video on the subject: Implementing UIViewController Containment
Further reading:
Container View Controller Examples
http://subjective-objective-c.blogspot.co.uk/2011/08/writing-high-quality-view-controller.html
Custom container view controller

How to Refer to the Current View Controller in the Code

I'm a new iOS developer with a simple question: I want to programmatically move from one view controller to the next, how do I write this code?
So far I have:
UINavigationController *navigationController;
navigationController = [[UINavigationController alloc] init];
[self.view addSubview:navigationController.view];
[navigationController pushViewController:viewController animated:NO];
I'm not even sure if this will work, ultimately, but my main question is "viewController" in line 4. The program doesn't know what that is. It is the name of my current view controller, but how do I set it up so that it knows what I mean by viewController?
As an aside, the above is part of an if/else statement that occurs and is connected to the NSUserDefaults class to make it such that the view controller I am referring to only loads if terms and conditions have not previously been accepted. Will that work? Thanks.
First the simple answer: pass self when you want to pass the "current" object.
The more important consideration is: is that nav controller on screen? It's likely that your code won't do much unless you use a navigation controller which is (probably) the window's root view controller.
This is fairly easy to setup in your storyboard ("embed in"->"navigation controller") and then you don't need to instantiate it in code, you simply use self.navigationController (usually).
Normally, you would instantiate viewController just before you push it, so the program will know what it is. And, sure, you can have an if statement, and push this new view controller based on how the if statement evaluates.
I'm not sure about the code you wrote -- whether that's right depends on the structure of your app, and where you're doing this. Often, the navigation controller is made the root view controller of the window, and you set the navigation controller with a root view controller of its own when you create it

what am I doing wrong in attempt to popup a web view and then allow user to go back

my app has tabBarController with 3 views and in one of them I want to popup a web browser with the ability to return back to the application. To do that I am using UINavigationController.
In myAppDelegate.h I have defined the property UINavigationController *nav and in myAppDelegate.m I have #synthesize nav.
In the class where the webPopup function resides upon pressing the button my code comes to this function.
- (IBAction)showWeb:(id)sender {
myAppDelegate *app=[[UINavigationController alloc] initWIthRootViewController:self];
// because I want to return back to the same view
webController *web = [[webController alloc] initWithStyle:UITableViewStypeGrouped];
[app.nav pushViewController:web animated:YES];
app.nav.view.frame = CGRect(,0,320,430);
[self.view.window addSUbview:app.nav.view];
}
The web popup occurs but it is moved vertically, when I press "back button" my former view appears as well and it is also shifted vertically from what it was before.
After going back and forth few times the thing hangs.
Questions:
1. what can cause the shift?
2. how to avoid when I go "back" to see the title(test from the "back"button, I think this might cause a shift when I go back.
3. how to find out why it hangs after few attempt?
Thanks.
Victor
The line:
myAppDelegate *app=[[UINavigationController alloc] initWIthRootViewController:self];
makes no sense to me. Surely your compiler is warning you about that? What is "myAppDelegate" defined as? Classes should have a capital letter at the front, by the way.
Also, the line
[self.view.window addSUbview:app.nav.view];
is highly suspect, because the UIWindow for your application should have only one child UIView. You shouldn't just add more views willy nilly and expect things to work. It is possible to change the child UIView by removing the old one and adding a new one, but you don't seem to be doing that. Having more than one child UIView of UIWindow gets you into trouble very quickly -- for example, device orientation changing can break.
I'm not exactly clear as to why the app delegate (or the window for that matter) needs to be messed with at all to do what you are trying to do. Sounds like you should just be using standard Nav View Controllers and Web Views.
Also, you are alloc init'ing w/o any memory management.

How do I create a UINavigationController with a default "back" state?

I have a UINavigationController, complete with table view and associated magic.
The data I'm populating that table view from may have items from multiple categories, but the default view for the user will be one in which they are viewing all of the items, and then they have the ability to move backwards to a different table view that would allow them to select a different category, which would then return to the original table view with the appropriate data populated.
What's the proper approach for this? I can't seem to wrap my head around how I would make the navigation controller give me a back button (with appropriately wired up actions) without having come from a previous view in the stack (which wouldn't really exist at launch time if I start the user off from what is essentially the detail view, in stack terms.)
Also, the back button should be titled "Groups", not "Back", but that's really just an implementation detail. :)
Update: This issue finally manifested itself in production code, and here’s how I fixed it:
My UINavigationController is created in a nib, with the root view set as the “groups” view. Then, in my app delegate, I push the second view onto the stack while the app is launching.
That works fine for achieving the proper stack, but that doesn’t help with the back button title, because the navigation controller didn’t seem to want to grab the title from the root view, and instead was showing a back button with “Item” as the title.
So, on the pushed view, in viewDidLoad, I set:
self.navigationController.navigationBar.backItem.title = #"Groups";
and that did the trick.
The only potential downside of doing it this way would be if the pushed view controller were ever used in a scenario where the view below it wasn’t the groups view, but since the design of this particular application ensures that never happens, I’m accepting that failure. ;)
Another update:
I’m an idiot. Just set the title property of the navigationItem provided by the navigationController in Interface Builder, and boom, no issue. Or do it in code. It doesn’t matter, just don’t do it by setting the backItem.title way I show you above. That’s just dumb.
In your application delegate's .m file in the application:didFinishLaunchingWithOptions: method just push your view controllers like you normally would with[self.navigationController pushViewController:your_view_controller animated:YES]; and it should push them on before the application's first view controller appears.
To change the text of the button to Groups just call this before pushing your controllers.:
UIBarButtonItem *newBackButton = [[UIBarButtonItem alloc] initWithTitle: #"Groups" style: UIBarButtonItemStyleBordered target: nil action: nil];
[[self navigationItem] setBackBarButtonItem: newBackButton];
[newBackButton release];