I have a UIViewController classes A and B. A loads B using: [A.view addSubView B.view].
B has a navigation bar with a Back button. I want to go back to A when I click it, so in the selector I tried [self.view removeFromSuperview], but it only removed the navigation bar. Then I tried [self.view.superview removeFromSuperview], it still just removed the navigation bar. What should I do?
Also, another minor issue with the Back button: setting it's title. I tried these two ways, but it still displays "Back".
navItem.backBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:#"Chapter" style:UIBarButtonItemStylePlain target:self action:#selector(handleBackBarButtonItem:)];
navItem.backBarButtonItem.title = #"Chapter";
Thank you in advance!
I don't think you quite understand how navigation (with UINavigationController) works in iOS. Assuming you want animation, this is what you want:
Set up a UINavigationController. This can be done in the app's delegate (to avoid memory leakage, set an instance variable on UINavigationController *navController:
navController = [[UINavigationController alloc] initWithRootViewController:A];
Note that we are adding A as our root view controller.
Push the second view controller when needed. I assume that you are adding B.view after a button is clicked or something. In the implementation of the method that adds the second view controller, run the following code, instead of [A.view addSubview:B.view]. This method should be in the first controller's .m file:
[self.navigationController pushViewController:B animated:YES];
This will also give a nice transition effect.
Pop the second view controller off the stack. With UINavigationController, a pretty arrow-shaped back button is automatically included in a pushed view controller, to navigate back to the last view controller. This means that you don't even need any code to allow backward navigation.
That's it! Now if you need to change the title of B's back button, do this in A's viewDidLoad method:
self.navigationItem.backBarButtonItem = customBackButtonItem;
You can get an array of subviews and then remove the ones you wanted to be removed. This SO post will show you how to remove all subviews or multiple subviews using subviews array.
Related
Here is my set up in storyboard.
I'm trying to use this code: (In an IBAction connected to the UIBarButtonItem in the last VC):
- (IBAction)confirmClicked:(UIBarButtonItem *)sender
{
//EXECUTE NAVIGATION
UITabBarController * tabControl = [self.storyboard instantiateViewControllerWithIdentifier:#"TabBarControl"];
tabControl.selectedIndex = 1;
[self presentViewController:tabControl animated:YES completion:nil];
}
to navigate from the last ViewController in this picture to that very first UINavigationController in the stack. I was informed that UINavigationControllers are not meant to be nested like this, and that only one UINavigationController should be necessary, but when I successfully remove(which I have done at least 5 times) the other three UINavigationControllers from the rest of the stack, I completely lose my UINavigationBars from the regular ViewControllers.
The problem with navigating the way that I currently am with my 'confirmClicked:' method, is that I create another instance of that first UIViewController in the hierarchy.
Should I:
A) Remove all the unnecessary UINavigationControllera in the view hierarchy, then use 'popToRootViewController:' to correctly navigate from the last VC to the first?
B) Try to navigate another way from the last VC to the first VC, keeping the extra UINavigationControllers in the view hierarchy?
Also, if I remove the Navigation Controllers, how will I keep my navigation bars in the UIViewControllers, since they have been disappearing in the past when removing the Navigation Controllers?
Sorry in advance for the many questions but I have been stuck for a while.
OK
First thing
Remove all those navigation controllers. You only need one.
Second
The code in that IBAction is creating a brand new instance of the TabBarController and placing it over the current stack (this is how to get memory problems).
Third
What you probably want is either an unwind segue (possibly) or code something like this...
- (IBAction)confirmClicked:(UIBarButtonItem *)sender
{
//EXECUTE NAVIGATION
UITabBarController * tabControl = [self.tabBarController setSelectedIndex:1];
}
when i read the api doc of the UINavigationController,the property navigationItem,has a tip:
Avoid tying the creation of bar button items in your navigation item to the creation of your view controller’s view
i don't understand what does this mean,can anybody explain this in detail
This was probably added to the documentation quite recently as I stumbled across this today for the first time. Moreover, in nearly every sample code that I've seen the initialization of the bar buttons happens in the viewDidLoad method - which is obviously not the best place to do this according to the quoted statement.
What Apple says us with this is that there can be situations where the content of the navigationItem is requested when the viewDidLoad method is not executed yet or not gets executed at all.
This happens when you push more than one viewcontroller at once. E.g. by using the setViewControllers:animated: interface or by doing something like this:
ViewController1 *firstViewController = [[ViewController1 alloc] init];
ViewController2 *secondViewController = [[ViewController2 alloc] init];
[navigationController pushViewController:firstViewController animated:YES];
[navigationController pushViewController:secondViewController animated:YES];
In this case the viewDidLoad method of firstViewController will not be called until the user navigates back to it. If you have set the title property in firstViewController, you would expect to have the back button labelled with the title you set in firstViewController. However the back button will be called "Back", as the title property of firstViewController is nil when UINavigationController asks for it.
The conclusion is: Tying the creation of bar button items to the creation of the view works for most situations. Nevertheless keep in mind that you can have situations where you need the navigationitem information before or without creating the view. In this case consider initializing the navigationItem property in the viewcontrollers init method.
I have an application created from the tabbed application template. (ARC, iOS 4)
There are several tabs and there is a button on the 2. tabs viewcontroller.view(ViewCont2).
This button loads another viewcontroller's(ModalViewCont) view by presentModalViewController method.
There is a close button on ModalViewCont which calls dismissModalViewControllerAnimated.
In viewDidDisappear of ViewCont2, i am setting self.view = nil and other outlets to nil to unload the view so it will be fresh loaded next time it appears on screen. I am doing this because it inherits from a base class(BaseViewCont) which initializes some general properties of the view controller and adds some buttons, labels etc. in viewDidLoad method. So, ViewControllers that inherit from this base class may configure those properties differently as they wish in their viewDidLoad method.
Problem
Now, when ModalViewCont on screen, pressing the Home button to put application in background and after getting the application back, closing the ModalViewCont does not bring back the ViewCont2's view but a black screen with the tabbar at the bottom. The same thing happens without putting the application background/foreground; if other tabs tapped before tapping the 2. tab.(EDIT : This happens only if self.view set to nil in viewWillDisappear instead of viewDidDisappear.)
I determined that ViewCont2 loads a new view (checked it's reference) but view's superview is nil so the new view is not displayed but a black screen.
Things that did not work
Using [self.view removeFromSuperview]; before setting self.view=nil,
In viewWillAppear adding view to the parent; [self.parentViewController.view addSubview:self.view]; This one did not work smoothly, view placed slightly up of the screen. This is because there are several other superviews in the hierarchy.
Solutions i considered;
1- If superview is nil in viewDidLoad, it becomes available in viewWillAppear (assumption). So, viewWillAppear method of ViewCont2 could be used to get the superview loaded correctly by the following;
_
if (self.view.superview == nil)
{
self.tabBarController.selectedViewController = nil;
self.tabBarController.selectedViewController = self;
}
2- viewWillAppear method of base class could be used instead for initialization so there is no need to unload the view. So, performance could be optimized, it will not be unloaded each time view disappears. Also, it would be better to perform initialization only once by checking a flag, instead of performing it every time it appears.
Questions
1- Why does not the superview restored? What should i do for it? (This is the main problem i want to understand and solve instead of trying alternatives...)
2- Am i doing something wrong by assigning nil to view for unloading it? If so, how should i unload the view properly in such case like this(tabbed application)?
3- Is anything wrong with the 1. solution? Does it seem like a kludge? Is that assumption about superview and viewWillAppear correct?
EDIT : It seems that when viewDidLoad is called earlier than it should(i.e when view nilled in viewWillDisappear instead of viewDidDisappear), superview is not set.
It seems weird, but your suggestion (1) is indeed a correct workaround for this problem:
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (!self.view.superview) { // check if view has been added to view hierarchy
self.tabBarController.selectedViewController = nil;
self.tabBarController.selectedViewController = self;
}
}
Your second suggestion is good for performance (because view loading is an expensive operation) - but it will not solve the problem. You can also end up with a black screen without setting the view to nil in the following situation (test this in the iOS simulator):
open the modal view
simulate a memory warning -> this will unload the views in the tabbarcontroller
press home button and open the app again
close modal view -> black screen
Generally you can assume that in viewDidLoad the view property is set and in viewWillAppear + viewDidAppear the view has been added to the view hierarchy; so the superview should be there at that time (Here the superview is a private view of the tabbarcontroller of class UIViewControllerWrapperView). However in our case, although the view is reloaded (at the time of app resume), it is not added to the view hierarchy resulting in a black screen. This seems to be a bug in UITabBarController.
The workaround forces the appearance selectors to be performed again. So viewWillAppear will be called again, this time with a superview in place. Also viewDidAppear will be called twice!
Setting self.view to nil is okay, but should not be necessary in most cases. Let the system decide when to unload the view (iOS can unload views when memory gets low). The view controller code should be designed in a way so that the UI can be reconfigured at any time without reloading the view.
You do not have full control over when views are loaded and unloaded, and you are not supposed to load/unload views manually yourself.
Instead, you should think of view loading/unloading as something that's entirely up to your UIViewControllers, with you being responsible only for:
Implementing the actual loading, by associating your UIViewController subclass with a nib file or by implementing loadView manually.
Optionally implementing the viewDidLoad, viewWillUnload and viewDidUnload callbacks, which are called by the view controller when it decides to load/unload its view.
The fact that you have no full control of when the above callbacks will be called, has implications about what should go into them.
In your case, if I understand correctly, whenever your ViewCont2's view disappears, you want to reset it so that when it reappears it will be in some "clean" state. I would implement this state reset in some method, and call it both from viewDidLoad and from viewDidDisappear. Alternatively, you can have the "clean" logic in viewWillAppear.
Or maybe you want to clean ViewCont2's view only when the present button is tapped? In that case, clean the view both in viewDidLoad, and when the button is tapped.
What I offer is that when the modal view controller is active, and you dismiss the view, that you add a new view to the navigation view controllers viewControllers, then that view is told to remove its predecessor.
You can play with my project to see if you think it works for you.
EDIT: my comment on the selected answer is that this technique obviously works now, but I myself am having a hard time followiing it. The code in my project uses the system in a simple and direct fashion - when the modal view is told to dismiss itself, it calls a method (could be in any class) that adds a new view to the navigation controller's array, then dismisses itself. For a bit of time there are two view controllers of the same time, the new one stacked over the old one. When the new view controller appears, based on seeing a flag it silently and behind the scenes removes the undesired viewController from the nab bar's stack, and poof, it goes away.
I have found the actual solution to the UITabBarController bug(memory warning,app enter back/foreground,dismiss modal). Using UITabBarController as the root view controller is the cause of the bug. So, we could use another view controller as the root view controller and present the tab bar from it. I have tested it on iOS 5.1 simulator.
Of course, the overhead of extra UIViewController is subject to debate. Also, it's against the Apple documentation;
Unlike other view controllers, a tab bar interface should never be installed as a child of another view controller.UITabBarController Class Reference
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
// A root view controller other than the actual UITabBarController is required.
self.window.rootViewController = [[UIViewController alloc] init];
[self.window makeKeyAndVisible];
self.tabBarController = [[UITabBarController alloc] init];
self.tabBarController.viewControllers = [NSArray arrayWithObjects:viewController1, ..., nil];
[self.window.rootViewController
presentModalViewController:self.tabBarController animated:NO];
}
I have found other solutions;
First one causes the warning: "Application windows are expected to have a root view controller at the end of application launch" although there is root view controller.
Although it seems kludgy, the temporary view controller will be released with the first one.
Second one seems more reasonable.
.
- (void) tabBarBlankScreenFix1
{
self.window.rootViewController = [[UIViewController alloc] init];
[self.window makeKeyAndVisible];
[self.window addSubview:self.tabBarController.view];
self.window.rootViewController = self.tabBarController;
}
- (void) tabBarBlankScreenFix2
{
self.window.rootViewController = [[UIViewController alloc] init];
[self.window makeKeyAndVisible];
[self.window addSubview:self.tabBarController.view];
}
I think you shouldn't assign the view to nil.
If I understand you right you want to refresh/reload content every time the view appears.
So instead of setting the view to nil, you should try to refresh it. You can do it by adding:
- (void)viewWillAppear{
[self.view setNeedsDisplay];}
Please tell me if I understand your issue right
I have a single view based app, when it runs it shows 2 button. On tap of 1 button i want to switch to another view which must be uinavigationcontroller and on 2 button i want to switch to tabbarcontroller view. I know what uinav and tabbar controllers can do. I created uinav and tab based project and study all the code, searched on internet for tutorials but what i get is everyone telling to add like this
self.window.rootViewController = self.navigationController;
on rootviewcontroller.
I dont want to add UInavigationController and tabbarcontroller on root view controller. Please help me solving this issue.
Thanks.
Then create a UIViewController called RootViewController for example, add two buttons inside its view and handle the touch events for those buttons. The first button, when touched should present your UINavigationController and the second button should present your UITabBarViewController. Then in your AppDelegate's didFinishLaunchingWithOptions: method initialize your RootViewController and set the self.window.rootViewController = rootViewController;
(UPDATE)
Allright, create a SingleView application. Put the buttons and set up the outlets and actions for those buttons. You also need to create 2 more view controllers: one UINavigationController and one UITabBarController. When the first button is touched, in the method which handles the touch add:
-(void)button1Touched:(id)sender
{
MyNavivationController *navc = [[MyNavigationController alloc] init];
[self presentViewController:navc animated:YES completion:nil];
}
You can do the same with the other button, but there initialize your tab bar controller and do the same. If you are unfamiliar with those operations you may refer to the documentation on how to create view controllers, handle events and etc...
#pamy I can provide you complete source code please let me know your skype id or other way to send you the file, i don't know how to post the code on stackoverflow.
I did write many times but it's not accepting my code.
I am an iOS development newbie. I am using the following code to set my backBarButtonItem -
UIBarButtonItem *temporaryBarButtonItem=[[UIBarButtonItem alloc] init];
temporaryBarButtonItem.title=#"Back";
self.navigationItem.backBarButtonItem = temporaryBarButtonItem;
[temporaryBarButtonItem release];
I also want it to execute another function, apart from going back. Is that possible? Or do I need another button to save?
You can practically do that in your viewDidDisappear or viewWillDisappear method.
If this view can only go back and doesn't present any views, then this should work.
However, if you plan on presenting a subview, modal view, go deeper in the navigation hierarchy, or do other view operations that will cause viewDidDisapper to get called, then you'll need to separate your back-button code logic somehow.
For instance if you will present a modal view from this view, you can check if self.modalViewController is nil, if it is then you have no modal view being present and can safely execute the back-button code. If it is not nil then you have a modal view present and should not execute back-button code. (viewWillDisappear should register the modal view controller as not-nil).
Very simple, try this!
[[UIBarButtonItem alloc] initWithTitle:#"Done" style:UIBarButtonItemStyleDone target:self action:#selector(myCoolAction:)];
Then all you do is change myCoolAction: to an appropriate method in your view controller and handle it. Enjoy!
EDIT: Oh, if you want to use this for the backBarButtonItem, it won't work how you expect. The system will not call your target/actions for the backBarButtonItem. They get cleared once assigned to the backBarButtonItem.
One way to handle this is too hook up the your UINavigationController as a delegate. Then in your
- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
Method you can do some custom behavior there.
Strictly speaking, this cannot be done with UIKit.
See the docs on backBarButtonItem:
The target and action of the back bar button item you set should be
nil. The default value is a bar button item displaying the navigation
item’s title.
The work around is to specify a leftBarButtonItem and attach whatever custom behaviour you require by attaching a #selector target.
The tricky bit is the button's image itself. As you know the default is not a rectangular button. Rather it has a left side arrow shape. For this you'll need to set a custom image to make the button appear to be the default.
There's tons of resources to do that out there including this one to extract all the UIKit artwork:
https://github.com/0xced/UIKit-Artwork-Extractor
You can add UIBarButtonItem in xib and add handler event there.
Else create UIBarButtonItem with a custom view, which is an UIButton and add event handler.
You should be able to do something like this instead of using viewDidDisappear or viewWillDisappear.
Place this in viewDidLoad:
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:#"Cancel" style:UIBarButtonItemStyleDone target:self action:#selector(cancelButtonAction:)];
self.navigationItem.leftItemsSupplementBackButton = NO;
Then you can create the method cancelButtonAction cancelButtonAction for your custom code and the back functionality like this:
- (void)cancelButtonAction:(id)sender {
//Your custom code goes here...
//This will perform the back functionality if using a Navigation Controller
[self.navigationController popViewControllerAnimated:YES];
}
Note: this will end up using a rectangular button, without the arrow.