I'm building a segmented control within my viewDidLoad method, like so:
NSArray *tabitems = [NSArray arrayWithObjects:#"ONE", #"TWO", nil];
UISegmentedControl *tabs = [[UISegmentedControl alloc] initWithItems:tabitems];
tabs.segmentedControlStyle = UISegmentedControlStyleBar;
tabs.frame = CGRectMake(185.0, 7.0, 130.0, 30.0);
tabs.selectedSegmentIndex = 0;
[self.navigationController.navigationBar addSubview:tabs];
[tabs release];
But when the user goes Back in the uinavigationcontroller hierarchy, the segmented controller stays on the navigation bar. How would I get rid of it? Or am I doing something fundamentally wrong?
EDIT
Following Alex's suggestions, I propertized tabs and tried:
NSArray *tabItems = [NSArray arrayWithObjects:#"FAQs", #"Terms", nil];
self.tabs = [[UISegmentedControl alloc] initWithItems:tabItems];
but I'm not sure it's a good idea to alloc a property;
And I'm using
[self.tabs removeFromSuperview];
in my viewWillDisappear. Is that enough?
Retain a reference to the segmented control in your view controller (i.e define tabs as a property in the view controller's header file).
Override the view controller's -viewWillDisappear: method, and remove the segmented control from the navigation bar there, using the control's -removeFromSuperview method.
EDIT
You would still alloc-init your segmented control tabs in -viewDidLoad. You just need to set up a retain property for tabs in your view controller's header, and move the control's release statement to the view controller's dealloc method.
Read the "Properties" section of this Objective-C tutorial for an introduction to properties and how to set them up.
The way to override a method is as follows:
- (ReturnClass) methodNameToOverride:args {
[super methodNameToOverride:args];
// your code goes here...
}
In the case of -viewWillDisappear:, this method gets called when your view controller is about to disappear, such as when it gets popped off the navigation stack. This is a great place to put code that manages clean-up of view-controller-specific items, like your segmented control:
- (void) viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[tabs removeFromSuperview];
}
EDIT 2
If your property is set as follows:
#property (nonatomic, retain) UISegmentedControl *tabs;
then you are going to retain anything you set self.tabs equal to.
Your code here:
self.tabs = [[UISegmentedControl alloc] initWithItems:...];
will create a memory leak, because you are retaining this object: [[UISegmentedControl alloc] init] — but you never release [[UISegmentedControl alloc] init] itself. This is bad.
Instead, use autorelease on the right side, i.e.:
self.tabs = [[[UISegmentedControl alloc] initWithItems:...] autorelease];
The tabs property retains its own reference to the initialized segmented control. That initialized segmented control is itself released properly at some later point. So no more memory leak.
Even better, set the UISegmentedControl's owning UIViewController's navigationItem.titleView to the UISegmentedControl.
-(void) viewDidLoad {
if(!mySegmentedControl) {
// initialize the UISegmentedControl
...
}
self.navigationItem.titleView = mySegmentedControl;
}
No manual removal of the UISegmentedControl or any other UIView for that matter required except, of course, releasing when owning UIViewController is dealloc'ed. Also, to be a "good memory citizen", you might set your property to nil in -viewDidUnload.
You can also customize the left and right barButtonItems of the UIViewController's navgiationItem.
Related
I would like to have a UISegmentedControl embedded in a PopoverController, similar to what is described in this SO question : UISegmentedControl embedded in a UINavigationBar/Item
The difference is that I have a different view controller for each view that I want to show in the popover, depending on the selected index on the Segmented Control. I'm not sure how I would go about doing this. Whenever I try to push a new view on top of the root view controller, the UISegmentedControl disappears. I would just like to switch between the two viewcontrollers, while keeping the UISegmentedControl visible. Is this even possible?
Thanks in advance!
If its a different viewController for each one of the segments on the segmentBar, you'll have to use a container viewController that adds the views of each of the viewController as a subview on itself or sets its view to that of the viewController's view. For example:
UIViewController* containerController = [[[UIViewController alloc] init] autorelease];
//Inside the viewDidLoad of the the ContainerController class, do the following:
//Initialize all three viewControllers
UIViewController* test1 = [[[UIViewController alloc] init] autorelease];
UIViewController* test1 = [[[UIViewController alloc] init] autorelease];
UIViewController* test1 = [[[UIViewController alloc] init] autorelease];
//set up the segment and add it to the container's navBar's title view.
[segmentedControl addTarget:self action:#selector(segmentValueChanged:) forControlEvents:UIControlEventValueChanged];
- (void)segmentValueChanged:(id)sender
{
//if first tab selected
[self.view removeAllSubviews];
[self.view addSubview:test1.view];
//if second tab selected
[self.view removeAllSubviews];
[self.view addSubview:test2.view];
//if third tab selected
[self.view removeAllSubviews];
[self.view addSubview:test3.view];
}
Instead of adding it as a subView, you might be able to just set self.view = test1.view. Obviously, you would use the container view to initialize the navController and put that navController inside the popover. Hope this helps!
If you are using presentModalViewController method to show your new view controller on the screen, it will always cover the entire screen and what ever is underneath it. That's just how it works.
As per docs:
On iPhone and iPod touch devices, the view of modalViewController is
always presented full screen. On iPad, the presentation depends on the
value in the modalPresentationStyle property.
The way to do it and still being able to control how the view controller is positioned is to create your own presentation method.
Original question is below.
I fixed my problem. I had the same view controller set for the tableview and the mapview. AND I had all of my mapView init in viewDidLoad. These both together made the following problems for me:
1) When the tab bar controller was presented modally and I received a memory warning the viewDidUnload was called (which I did nothing in before). When the modal tab bar controller was dismissed and viewDidLoad was called, it reset my mapView.
2) When the tab bar controller was presented modally and I received a memory warning before moving to the tab with the view controller and then switched to that tab, my viewDidLoad was called in my mapView's controller which reset my map, AND it didn't reset the tables properties so that my tableView would not work.
Much thanks to Anomie for helping me debug this issue, and gain a better understanding of how these pieces are all connected. Things work great now, and my App has a better design.
Original Question:
I have an application that has a mapview and when I present a tab bar view controller modally, every once in a while when I cancel the modal view controller the map resets back to the world view like a reset button was pressed on it. A few details:
This NEVER happens on the simulator
I notice memory warnings around the time this happens on my device
I do not do anything when memory warnings happen in my app, so nothing should have touched the mapview
Also, my tableview in my modal view controller sometimes shows up blank as well (aroudn the time of memory warnings). When it comes up blank, the methods normally called to get the number of rows, sections, and data are not called at all. Upon cancelling the modal view controller and re-opening it, the data is there fine, so the data is not getting erased..
Code I used to create the tab bar controller, picker and table:
ABPeoplePickerNavigationController *picker = [[ABPeoplePickerNavigationController alloc] init];
picker.peoplePickerDelegate = self;
// Display only a person's address(es)
NSArray *displayedItems = [NSArray arrayWithObjects:[NSNumber numberWithInt:kABPersonAddressProperty],
nil];
picker.displayedProperties = displayedItems;
UITabBarItem *peoplePickerTabBarItem = [[UITabBarItem alloc] initWithTabBarSystemItem:UITabBarSystemItemContacts tag:0];
picker.tabBarItem = peoplePickerTabBarItem;
UITableViewController *tvc = [[UITableViewController alloc] initWithStyle:UITableViewStylePlain];
tvc.tableView.delegate = self;
tvc.tableView.dataSource = self;
UINavigationController *nvc = [[UINavigationController alloc] initWithRootViewController:tvc];
UIBarButtonItem *uibbiCancel = [[UIBarButtonItem alloc] initWithTitle:#"Cancel" style:UIBarButtonItemStylePlain target:self action:#selector(cancelTable)];
tvc.navigationItem.rightBarButtonItem = uibbiCancel;
tvc.title = #"Recents";
UITabBarItem *nvcTabBarItem = [[UITabBarItem alloc] initWithTabBarSystemItem:UITabBarSystemItemRecents tag:2];
nvc.tabBarItem = nvcTabBarItem;
tbc = [[UITabBarController alloc] init];
NSArray *sections = [[NSArray alloc] initWithObjects:picker, nvc, nil];
[tbc setViewControllers:sections];
[self presentModalViewController:tbc animated:YES];
[nvcTabBarItem release];
[uibbiCancel release];
[tvc release];
[peoplePickerTabBarItem release];
[picker release];
[nvc release];
[sections release];
[tbc release];
For your map view, what is probably happening is this: When a UIViewController is not being actively displayed (e.g. it is in a non-displayed tab of a UITabViewController, not the visible view in a UINavigationController, or is hidden by a modally presented view controller), it may release its view if a memory notification occurs. It will then recreate the view when needed.
For your table view, I'm not sure. Are you assigning the data source and delegate to the table view in the code that creates the containing view controller, rather than doing so in the view controller's viewDidLoad method?
I know the SDK documentation says
Taps outside of the popover’s contents automatically dismiss the popover.
But I'm sure the smart people here found a way :)
maybe I should overwrite the popover dismiss function?
Thanks
EDIT:
I tried using the passthroughViews as was suggested here, and it works perfectly. Here's the code for whoever needs it - in this example, I put self.view in the array, which means that where ever outside the button where the popover was originated, nothing dismiss the popover.
popoverController.passthroughViews = [[[NSArray alloc] initWithObjects:self.view, nil] autorelease];
You need to set the passthroughViews property. From the documentation:
An array of views that the user can interact with while the popover is visible.
#property (nonatomic, copy) NSArray *passthroughViews
When a popover is active, interactions with other views are normally disabled until the popover is dismissed. Assigning an array of views to this property allows taps outside of the popover to be handled by the corresponding views.
Set passthroughViews to an array of view(s) that you want to handle the touch event instead of just dismissing the popover.
There is a very simple and legit solution. In the view controller that presents your UIPopoverController, conform to the UIPopoverControllerDelegate protocol and implement the following delegate method. I just tested this and it does prevent popover to dismiss.
- (BOOL)popoverControllerShouldDismissPopover:(UIPopoverController *)popoverController
{
return NO;
}
Just make sure that you have set the delegate of your popover controller to the view controller that implements this.
You can dismiss the popover by using [popoverController dismissPopoverAnimated:NO]; method.
The accepted answer does not really answer the question, "is there a way NOT to have the popover dismissed when pressing outside it?", imo. It does give a possible view but could require hackish access to all parent views and determining what views are on the screen etc. The question could be rephrased as, "how do I make a popover view modal?"
You would do this like so, with a done button to close the popover:
UIViewController* vc = [[[UIViewController alloc] init] autorelease];
UIBarButtonItem* doneButton = [[[UIBarButtonItem alloc] initWithTitle:#"Done"] style:UIBarButtonItemStyleDone target:self action:#selector(processDoneAction)] autorelease];
[vc.navigationItem setLeftBarButtonItem:doneButton];
vc.modalInPopover = YES;
//If you want full screen:
vc.modalPresentationStyle = UIModalPresentationFullScreen;
vc.wantsFullScreenLayout = YES;
UINavigationController* navC = [[[UINavigationController alloc] initWithRootViewController:vc] autorelease];
UIView* view = create your view
vc.view = view;
UIPopoverController* pc = [[[UIPopoverController alloc] initWithContentViewController:navC] autorelease];
pc.delegate = self;
self.popoverController = pc;
Then you'll in your processDoneAction method you will need to dismiss the popover. Other considerations would be dismissing and redisplaying on device orientation changes, but I will leave that to another exercise as that has been answered previously on stackoverflow.
Someone has posted a similar question to this with no resolution, but also no sample code. So, I thought I would post my problem in detail here.
I have a game with several modes of play, each of these having several options. After playing around with various designs, it seems cleanest to put them into a UITabBarController with three tabs, one for each class of games. I created a new UIVIewController which is loaded from the main menu screen (replacing the main screen) and initializes the UITabBarController as follows:
barController = [[UITabBarController alloc] init];
Games1 *vc1 = [[[Games1 alloc] initWithNibName:#"Games1" bundle:nil] autorelease];
Games2 *vc2 = [[[Games2 alloc] initWithNibName:#"Games2" bundle:nil] autorelease];
Games3 *vc3 = [[[Games3 alloc] initWithNibName:#"Games3" bundle:nil] autorelease];
NSArray* controllers = [NSArray arrayWithObjects:vc3, vc1, vc2, nil];
barController.viewControllers = controllers;
[self.view addSubview:barController.view];
When the user selects a game, I remove the UIViewController from the window and deallocate as follows:
- (void)dealloc {
printf("Games released: barController: %d\n", [barController retainCount]);
[barController.view removeFromSuperview];
barController.viewControllers = 0;
[barController release];
barController = 0;
[super dealloc];
}
The problem I have is that when I rotate the device, I get a crash. If I launch a game mode directly from the main screen and rotate, no crash. I've verified that everything is getting deallocated, and my retain count on the bar controller is 1. Any suggestions on how to eliminate this crash? Thanks!
[Edit] A bit more info:
The barController is defined as:
IBOutlet UITabBarController *barController;
with:
#property (nonatomic, retain) IBOutlet UITabBarController *barController;
It ends up the problem was only peripherally related to the UITabBarController. I was adding and removing UIViewControllers directly to my app window, which has been shown to be causing problems elsewhere. Adding a master UIViewController / UIView and only adding and removing from that fixes everything, although an autorelease instead of a release may have worked as well. See the discussion here:
View Controller being sent a message even though it has been deallocated
The UITabBarController was just causing the problem to happen much more quickly and obviously.
Don't do this:
barController.viewControllers = 0;
In -dealloc you should only remove the UITabBarController's view from its superview and release it.
How can I make it so when a tab is selected, the current one is unloaded, and the next one is loaded so only one loaded at a time? Or should I not even do this? I know how to do it with a normal UIViewController as the root VC, but not sure with a UITabBarController. Also, is there a way to animate the transition from one tab to the next? Any help? Thanks!!
EDIT: ... If I unload the view controllers, then their icons on the tab bar are gone... maybe I'll just unload their views..
I can answer both questions in one...
You just need a class that acts as the UITabBarController delegate, then implement a method like so:
// Animate tab selections so they fade in and fade out
-(void)tabBarController:(UITabBarController*)tbc didSelectViewController:(UIViewController*)newSelection
{
[UIView beginAnimations:#"TabFadeIn" context:nil];
[UIView setAnimationDuration:0.6];
for( UIViewController* vc in tbc.viewControllers )
vc.view.alpha = (vc==newSelection) ? 1 : 0;
[UIView commitAnimations];
}
Now my code simply makes the tab bars fade in and out, but you could also do work here to unload non-used tabs. Sometimes that is a good idea if some of the tabs will be using a ton of memory.
You cant really manage the UITabBarController unfortunaly so you cant do lazy loading. You can by managining your own TabBar but you said u knew that already,
to manage your own tab bar though all you gotta do is setup a UITabBar with its TabBarItems in a ViewController, then implement the TabBar Delegate protocol, mainly the – tabBar:didSelectItem: method which is called whenever the tabbarItem selection is changed, then based on the item id you can load your new ViewController and release any others
so: Edit: this code goes in your UIViewController
-(void)addTabBar{
NSMutableArray* items=[[NSMutableArray alloc] init];
UITabBarItem *eventsItem= [[UITabBarItem alloc] initWithTitle:#"Events" image:nil tag:0];
UITabBarItem *albumItems=[[UITabBarItem alloc] initWithTitle:#"Album" image:nil tag:1]; //the tag is how you tell what was clicked
[items addObject:homeItem];
[items addObject:albumItems];
//MyTabBar is of type UITabBar
myTabBar=[[UITabBar alloc] initWithFrame:CGRectMake(0,411,320,49)];
[myTabBar setItems:items];
myTabBar.delegate=self; //you gotta implement the UITabBar delegate protocol
[myTabBar setSelectedItem:eventItem]; //set the selected item
[homeItem release];
[eventsItem release];
[albumItems release];
[items release];
[self.view addSubview:myTabBar]
}
then the protocol method would look something like below
- (void)tabBar:(UITabBar *)tabBar didSelectItem:(UITabBarItem *)item
{
if(item.tag == 0 )
{
//load the ViewController that pertains to this item and release others
}
...etc
}
Lazy loading is not an UITabBarController task. Instead, it is responsability of your viewControllers associated with your Tab.
To release the UIView, associated with each UIViewControllers, every time you change the TabBarItem, you must implement the following method in each UIViewController subclass, associated with your UITabBarController.viewControllers property:
-(void)viewDidDisappear {
[self.view removeFromSuperview];
self.view = nil;
}
Obviously, this will remove the self.view associated with your UIViewController. However, if your code is smart enough, this will remove all the related objects.
For example, suppose that your loadView method is as follow:
-(void)loadView {
UIView *contentVew = [[UIView alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
self.view = contentView;
…
...
UILabel *aLabel = [[UILabel alloc] initWithFrame:CGRectMake(0,0,320,50)];
…
…
[contentView addSubview:aLabel];
[aLabel release];
…
[contentView release];
}
This means that every object inside the contentView and their memory responsabilities are demanded to the contentView, that is released and attached to the self.view property.
In this scenario, removing the self.view (that's the reference to the contentView) resulting in a domino-style releasing of every object, that's your goal.
Best regards
Not sure why you'd want to do this, the current tab will get unloaded anyway if there's a memory issue involved. That's what -viewWillAppear, -viewDidUnload, etc. are for.
UITabBarController does lazy load all of its view controllers. When a tab is switched out, then it's view is subject to being deallocated in a memory tight situation. It is then recreated when it is chosen the second time. Furthermore, most of your memory hits are in your views and not the view controllers. Hence, don't worry about the memory hit from the view controller. The view is the proze.
If you are running on v3 of the OS, then you can use the -viewDidUnload method to ensure the maximal amount of memory reduction.
Andrew
I'm currently using this to unload inactive view controllers in the tab bar (based on Kendall's answer)
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController: (UIViewController *)viewController {
// reload all inactive view controllers in the tab bar
for (UIViewController *vc in tabBarController.viewControllers) {
if(vc != viewController)
[vc didReceiveMemoryWarning];
}
}