views:

1573

answers:

2

Hi all,

I'm trying to work out the "best" way to use a UISegmentedControl for an iPhone application. I've read a few posts here on stackoverflow and seen a few people's ideas, but I can't quite sort out the best way to do this. The posts I'm referring to are:

http://stackoverflow.com/questions/1559794/changing-views-from-uisegmentedcontrol and http://stackoverflow.com/questions/1047114/how-do-i-use-a-uisegmentedcontrol-to-switch-views

It would seem that the options are:

  • Add each of the views in IB and lay them out on top of each other then show/hide them
  • Create each of the subviews separately in IB, then create a container in the main view to populate with the subview that you need
  • Set up one really tall or really wide UIView and animate it left/right or up/down depending on the selected segment
  • Use a UITabBarController to swap out the subviews - seems silly
  • For tables, reload the table and in cellForRowAtIndex and populate the table from different data sources or sections based on the segment option selected (not the case for my app)

So which approach is best for subview/non-table approaches? Which is the easiest to implement? Could you share some sample code to the approach?

Thanks!

+2  A: 

I'd go with the second option you mention, creating the subviews in IB and swapping them in and out of a main view. This would be a good opportunity to use UIViewController, unsubclassed: in your initial setup, create a controller using -initWithNibName:bundle: (where the first parameter is the name of the NIB containing the individual subview, and the second parameter is nil) and add its view as a subview of your main view as necessary. This will help keep your memory footprint low: the default behavior of a UIViewController when receiving a memory warning is to release its view if it has no superview. As long as you remove hidden views from the view hierarchy, you can keep the controllers in memory and not worry about releasing anything.

(edited in response to comment:)

You don't need to subclass UIViewController, but you do need separate XIBs for each view. You also don't need to add anything to the containing view in IB.

Instance variables, in the interface of whatever class is handling all this:

 UIViewController *controllerOne;
 UIViewController *controllerTwo;

 UIViewController *currentController;

 IBOutlet UIView *theContainerView;

In your setup (-applicationDidFinishLaunching: or whatever)

 controllerOne = [[UIViewController alloc] initWithNibName:@"MyFirstView" bundle:nil];
 controllerTwo = [[UIViewController alloc] initWithNibName:@"MySecondView" bundle:nil];

To switch to a controller:

 - (void)switchToController:(UIViewController *)newCtl
 {
      if(newCtl == currentController)
           return;
      if([currentController isViewLoaded])
           [currentController.view removeFromSuperview];

      if(newCtl != nil)
           [theContainerView addSubview:newCtl.view];

      currentController = newCtl;
 }

Then just call that with, e.g.,

 [self switchToController:controllerOne];
Noah Witherspoon
Just to clarify... Are you saying to create a separate UIViewController .h, .m, and .xib file in IB for each subview, instantiate one of each of them in the viewDidLoad of the "containing" view, add a UIView in IB to the containing view, then do what??
Neal L
+2  A: 

I've come across this requirement as well in an iPad application.

The solution I came to was to create specialized view controllers for each style of view to handle business logic relating to those views (ie. relating to each segment), and programatically add/remove them as subviews to a 'managing' controller in response to selected segment index changes.

To do this, one has to create an additional UIViewController subclass that manages UISegmentedControl changes, and adds/removes the subviews.

The code below does all this, also taking care of a few caveats/extras:

  • viewWillAppear/viewWillDisappear/etc, aren't called on the subviews automatically, and need to be told via the 'managing' controller
  • viewWillAppear/viewWillDisappear/etc, aren't called on 'managing' controller when it's within a navigation controller, hence the navigation controller delegate
  • If you'd like to push onto a navigation stack from within a segment's subview, you need to call back on to the 'managing' view to do it, since the subview has been created outside of the navigation hierarchy, and won't have a reference to the navigation controller.
  • If used within a navigation controller scenario, the back button is automatically set to the name of the segment.

Interface:

@interface SegmentManagingViewController : UIViewController <UINavigationControllerDelegate> {
    UISegmentedControl    * segmentedControl;
    UIViewController      * activeViewController;
    NSArray               * segmentedViewControllers;
}

@property (nonatomic, retain) IBOutlet UISegmentedControl * segmentedControl;
@property (nonatomic, retain) UIViewController            * activeViewController;
@property (nonatomic, retain) NSArray                     * segmentedViewControllers;

@end

Implementation:

@interface SegmentManagingViewController ()
- (void)didChangeSegmentControl:(UISegmentedControl *)control;
@end

@implementation SegmentManagingViewController

@synthesize segmentedControl, activeViewController, segmentedViewControllers;

- (void)viewDidLoad {
    [super viewDidLoad];

    UIViewController * controller1 = [[MyViewController1 alloc] initWithParentViewController:self];
    UIViewController * controller2 = [[MyViewController2 alloc] initWithParentViewController:self];
    UIViewController * controller3 = [[MyViewController3 alloc] initWithParentViewController:self];

    self.segmentedViewControllers = [NSArray arrayWithObjects:controller1, controller2, controller3, nil];
    [controller1 release];
    [controller2 release];
    [controller3 release];

    self.navigationItem.titleView = self.segmentedControl =
    [[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:@"Seg 1", @"Seg 2", @"Seg 3", nil]];
    self.segmentedControl.selectedSegmentIndex = 0;
    self.segmentedControl.segmentedControlStyle = UISegmentedControlStyleBar;

    [self.segmentedControl addTarget:self action:@selector(didChangeSegmentControl:) forControlEvents:UIControlEventValueChanged];

    [self didChangeSegmentControl:self.segmentedControl]; // kick everything off
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    [self.activeViewController viewWillAppear:animated];
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    [self.activeViewController viewDidAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.activeViewController viewWillDisappear:animated];
}

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [self.activeViewController viewDidDisappear:animated];
}

#pragma mark -
#pragma mark UINavigationControllerDelegate control

// Required to ensure we call viewDidAppear/viewWillAppear on ourselves (and the active view controller)
// inside of a navigation stack, since viewDidAppear/willAppear insn't invoked automatically. Without this
// selected table views don't know when to de-highlight the selected row.

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    [viewController viewDidAppear:animated];
}

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    [viewController viewWillAppear:animated];
}

#pragma mark -
#pragma mark Segment control

- (void)didChangeSegmentControl:(UISegmentedControl *)control {
    if (self.activeViewController) {
        [self.activeViewController viewWillDisappear:NO];
        [self.activeViewController.view removeFromSuperview];
        [self.activeViewController viewDidDisappear:NO];
    }

    self.activeViewController = [self.segmentedViewControllers objectAtIndex:control.selectedSegmentIndex];

    [self.activeViewController viewWillAppear:NO];
    [self.view addSubview:self.activeViewController.view];
    [self.activeViewController viewDidAppear:NO];

    NSString * segmentTitle = [control titleForSegmentAtIndex:control.selectedSegmentIndex];
    self.navigationItem.backBarButtonItem  = [[UIBarButtonItem alloc] initWithTitle:segmentTitle style:UIBarButtonItemStylePlain target:nil action:nil];
}

#pragma mark -
#pragma mark Memory management

- (void)dealloc {
    self.segmentedControl = nil;
    self.segmentedViewControllers = nil;
    self.activeViewController = nil;
    [super dealloc];
}

@end

Hope this helps.

crafterm
Voted up! There is a small memory leak in -didChangeSegmentedControl: method. The last statement should be: self.navigationItem.backBarButtonItem = [[[UIBarButtonItem alloc] initWithTitle:segmentTitle style:UIBarButtonItemStylePlain target:nil action:nil] autorelease]; A similar memory leak is in viewDidLoad: self.navigationItem.titleView = self.segmentedControl = [[[UISegmentedControl alloc] initWithItems:[NSArray arrayWithObjects:@"Seg 1", @"Seg 2", @"Seg 3", nil]] autorelease];
Mustafa