This problem is driving me crazy. I'm trying to change the viewController when the user changes the selected "tab" of the segmented control. I've spent a couple hours researching and haven't been able to find an answer that works or is done through storyboard.
It really bother me since setting a tab application is so easy, but trying to use the segmented control like the tab application is just not working. I already know how to detect which index is selected in the segmented control. How can I achieve this?
Thank you very much.
NOTE: Answer updated with view controller containment code for iOS 5+ including #interface section
In an app of mine, I have a view controller with a Segment Control in the Navigation Bar and clicking on the "tabs" switches view controllers. The basic idea is to have an array of view controllers and switch between them using the Segment Index (and the indexDidChangeForSegmentedControl IBAction.
Example code (iOS 5 or later) from my app (this is for 2 view controllers but it's trivially extended to multiple view controllers); the code is slightly longer than for iOS 4 but will keep the object graph intact. Also, it uses ARC:
#interface MyViewController ()
// Segmented control to switch view controllers
#property (weak, nonatomic) IBOutlet UISegmentedControl *switchViewControllers;
// Array of view controllers to switch between
#property (nonatomic, copy) NSArray *allViewControllers;
// Currently selected view controller
#property (nonatomic, strong) UIViewController *currentViewController;
#end
#implementation UpdateScoreViewController
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
[super viewDidLoad];
// Create the score view controller
ViewControllerA *vcA = [self.storyboard instantiateViewControllerWithIdentifier:#"ViewControllerA"];
// Create the penalty view controller
ViewControllerB *vcB = [self.storyboard instantiateViewControllerWithIdentifier:#"ViewControllerB"];
// Add A and B view controllers to the array
self.allViewControllers = [[NSArray alloc] initWithObjects:vcA, vcB, nil];
// Ensure a view controller is loaded
self.switchViewControllers.selectedSegmentIndex = 0;
[self cycleFromViewController:self.currentViewController toViewController:[self.allViewControllers objectAtIndex:self.switchViewControllers.selectedSegmentIndex]];
}
#pragma mark - View controller switching and saving
- (void)cycleFromViewController:(UIViewController*)oldVC toViewController:(UIViewController*)newVC {
// Do nothing if we are attempting to swap to the same view controller
if (newVC == oldVC) return;
// Check the newVC is non-nil otherwise expect a crash: NSInvalidArgumentException
if (newVC) {
// Set the new view controller frame (in this case to be the size of the available screen bounds)
// Calulate any other frame animations here (e.g. for the oldVC)
newVC.view.frame = CGRectMake(CGRectGetMinX(self.view.bounds), CGRectGetMinY(self.view.bounds), CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds));
// Check the oldVC is non-nil otherwise expect a crash: NSInvalidArgumentException
if (oldVC) {
// Start both the view controller transitions
[oldVC willMoveToParentViewController:nil];
[self addChildViewController:newVC];
// Swap the view controllers
// No frame animations in this code but these would go in the animations block
[self transitionFromViewController:oldVC
toViewController:newVC
duration:0.25
options:UIViewAnimationOptionLayoutSubviews
animations:^{}
completion:^(BOOL finished) {
// Finish both the view controller transitions
[oldVC removeFromParentViewController];
[newVC didMoveToParentViewController:self];
// Store a reference to the current controller
self.currentViewController = newVC;
}];
} else {
// Otherwise we are adding a view controller for the first time
// Start the view controller transition
[self addChildViewController:newVC];
// Add the new view controller view to the ciew hierarchy
[self.view addSubview:newVC.view];
// End the view controller transition
[newVC didMoveToParentViewController:self];
// Store a reference to the current controller
self.currentViewController = newVC;
}
}
}
- (IBAction)indexDidChangeForSegmentedControl:(UISegmentedControl *)sender {
NSUInteger index = sender.selectedSegmentIndex;
if (UISegmentedControlNoSegment != index) {
UIViewController *incomingViewController = [self.allViewControllers objectAtIndex:index];
[self cycleFromViewController:self.currentViewController toViewController:incomingViewController];
}
}
#end
Original example (iOS 4 or before):
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
[super viewDidLoad];
// Create the score view controller
AddHandScoreViewController *score = [self.storyboard instantiateViewControllerWithIdentifier:#"AddHandScore"];
// Create the penalty view controller
AddHandPenaltyViewController *penalty = [self.storyboard instantiateViewControllerWithIdentifier:#"AddHandPenalty"];
// Add Score and Penalty view controllers to the array
self.allViewControllers = [[NSArray alloc] initWithObjects:score, penalty, nil];
// Ensure the Score controller is loaded
self.switchViewControllers.selectedSegmentIndex = 0;
[self switchToController:[self.allViewControllers objectAtIndex:self.switchViewControllers.selectedSegmentIndex]];
}
#pragma mark - View controller switching and saving
- (void)switchToController:(UIViewController *)newVC
{
if (newVC) {
// Do nothing if we are in the same controller
if (newVC == self.currentViewController) return;
// Remove the current controller if we are loaded and shown
if([self.currentViewController isViewLoaded]) [self.currentViewController.view removeFromSuperview];
// Resize the new view controller
newVC.view.frame = CGRectMake(CGRectGetMinX(self.view.bounds), CGRectGetMinY(self.view.bounds), CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds));
// Add the new controller
[self.view addSubview:newVC.view];
// Store a reference to the current controller
self.currentViewController = newVC;
}
}
- (IBAction)indexDidChangeForSegmentedControl:(UISegmentedControl *)sender {
NSUInteger index = sender.selectedSegmentIndex;
if (UISegmentedControlNoSegment != index) {
UIViewController *incomingViewController = [self.allViewControllers objectAtIndex:index];
[self switchToController:incomingViewController];
}
}
I'd say it's much simpler to change subviews within a UIViewController, you can set up your subviews in your storyboards and hook them up with IBOulets in your controller you can set the hidden property of your views to YES or NO depending on the control that was clicked.
Now, if you use #Robotic Cat's approach which is also a good solution you can have a little more modularity in how your app works, considering you'd have to place all your logic in one controller using the solution I presented.
UISegmentedControl is a little different in that it doesn't have a delegate protocol, you have to use the "add target" style. In your case what you want to do is add a target to be notified when the UISegmentedControl changes (which is likely the parent view controller), and then that target can deal with the tab switching.
For example:
[self.mainSegmentedControl addTarget:self action:#selector(changedSegmentedControl:) forControlEvents:UIControlEventValueChanged];
In this example, the code is being invoked from some view/controller that has access to the variable for the segmented control. We add ourself to get the changedSegmentedControl: method invoked.
Then you would have another method like so:
- (void)changedSegmentedControl:(id)sender
{
UISegmentedControl *ctl = sender;
NSLog(#"Changed value of segmented control to %d", ctl.selectedSegmentIndex);
// Code to change View Controller goes here
}
Note: this is untested code written from memory -- please consult the docs accordingly.
Take a look at this pod: https://github.com/xmartlabs/XLMailBoxContainer. It makes the UI animation among the view controllers. These view controller can extend UITableViewController or any other view controller.
I hope this help you!
Related
I have a few ViewControllers that all have buttons which should segue to some others. There will never be a back button, but instead everything is connected through a bunch of loops so that there is never a dead end. So I'd like to fully transition from one View Controller to another, and have the old View Controller be completely deleted. There is no hierarchy and no parent/child relationship between the View Controllers. How should I handle this situation?
Instantiate the view controller you want to go to, then set it as the window's root view controller.
NextViewController *next = [self.storyboard instantiateViewControllerWithIdentifier:#"Next"]; // or other instantiation method depending on how you create your controller
self.view.window.rootViewController = next;
You could do this with custom segues if you want to show the flow from controller to controller in your storyboard (you wouldn't need any code at all then). The custom segue's perform method would look like this,
#implementation RootVCReplaceSegue
-(void)perform {
UIViewController *source = (UIViewController *)self.sourceViewController;
source.view.window.rootViewController = self.destinationViewController;
}
If you want a fade animation, you can add a snapshot of the source view controller as a subview of the destination view controller's view, then fade it out,
-(void)perform {
UIViewController *source = (UIViewController *)self.sourceViewController;
UIView *sourceView = [source.view snapshotViewAfterScreenUpdates:YES];
[[self.destinationViewController view] addSubview:sourceView];
source.view.window.rootViewController = self.destinationViewController;
[UIView animateWithDuration:.5 animations:^{
sourceView.alpha = 0;
} completion:^(BOOL finished) {
[sourceView removeFromSuperview];
}];
}
UPDATE
Based on Tim's answer, I implemented the following in each view controller that had a scrollview (or subclass) that was part of my custom container:
- (void)didMoveToParentViewController:(UIViewController *)parent
{
if (parent) {
CGFloat top = parent.topLayoutGuide.length;
CGFloat bottom = parent.bottomLayoutGuide.length;
// this is the most important part here, because the first view controller added
// never had the layout issue, it was always the second. if we applied these
// edge insets to the first view controller, then it would lay out incorrectly.
// first detect if it's laid out correctly with the following condition, and if
// not, manually make the adjustments since it seems like UIKit is failing to do so
if (self.collectionView.contentInset.top != top) {
UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0);
self.collectionView.contentInset = newInsets;
self.collectionView.scrollIndicatorInsets = newInsets;
}
}
[super didMoveToParentViewController:parent];
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
I have custom container view controller called SegmentedPageViewController. I set this as a UINavigationController's rootViewController.
The purpose of SegmentedPageViewController is to allow a UISegmentedControl, set as the NavController's titleView, to switch between different child view controllers.
These child view controllers all contain either a scrollview, tableview, or collection view.
We're finding that the first view controller loads fine, correctly positioned underneath the navigation bar. But when we switch to a new
view controller, the navbar isn't respected and the view is set underneath the nav bar.
We're using auto layout and interface builder. We've tried everything we can think of, but can't find a consistent solution.
Here's the main code block responsible for setting the first view controller and switching to another one when a user taps on the segmented control:
- (void)switchFromViewController:(UIViewController *)oldVC toViewController:(UIViewController *)newVC
{
if (newVC == oldVC) return;
// Check the newVC is non-nil otherwise expect a crash: NSInvalidArgumentException
if (newVC) {
// Set the new view controller frame (in this case to be the size of the available screen bounds)
// Calulate any other frame animations here (e.g. for the oldVC)
newVC.view.frame = self.view.bounds;
// Check the oldVC is non-nil otherwise expect a crash: NSInvalidArgumentException
if (oldVC) {
// **** THIS RUNS WHEN A NEW VC IS SET ****
// DIFFERENT FROM FIRST VC IN THAT WE TRANSITION INSTEAD OF JUST SETTING
// Start both the view controller transitions
[oldVC willMoveToParentViewController:nil];
[self addChildViewController:newVC];
// Swap the view controllers
// No frame animations in this code but these would go in the animations block
[self transitionFromViewController:oldVC
toViewController:newVC
duration:0.25
options:UIViewAnimationOptionLayoutSubviews
animations:^{}
completion:^(BOOL finished) {
// Finish both the view controller transitions
[oldVC removeFromParentViewController];
[newVC didMoveToParentViewController:self];
// Store a reference to the current controller
self.currentViewController = newVC;
}];
} else {
// **** THIS RUNS WHEN THE FIRST VC IS SET ****
// JUST STANDARD VIEW CONTROLLER CONTAINMENT
// Otherwise we are adding a view controller for the first time
// Start the view controller transition
[self addChildViewController:newVC];
// Add the new view controller view to the view hierarchy
[self.view addSubview:newVC.view];
// End the view controller transition
[newVC didMoveToParentViewController:self];
// Store a reference to the current controller
self.currentViewController = newVC;
}
}
}
Your custom container view controller will need to adjust the contentInset of the second view controller according to your known navigation bar height, respecting the automaticallyAdjustsScrollViewInsets property of the child view controller. (You may also be interested in the topLayoutGuide property of your container - make sure it returns the right value during and after the view switch.)
UIKit is remarkably inconsistent (and buggy) in how it applies this logic; sometimes you'll see it perform this adjustment automatically for you by reaching multiple view controllers down in the hierarchy, but often after a custom container switch you'll need to do the work yourself.
This seems to all be a lot simpler than what people make out.
UINavigationController will set scrollview insets only while laying out subviews. addChildViewController: does not cause a layout, however, so after calling it, you just need to call setNeedsLayout on your navigationController. Here's what I do while switching views in a custom tab-like view:
[self addChildViewController:newcontroller];
[self.view insertSubview:newview atIndex:0];
[self.navigationController.view setNeedsLayout];
The last line will cause scrollview insets to be re-calculated for the new view controller's contents.
FYI in case anyone is having a similar problem: this issue can occur even without embedded view controllers. It appears that automaticallyAdjustsScrollViewInsets is only applied if your scrollview (or tableview/collectionview/webview) is the first view in their view controller's hierarchy.
I often add a UIImageView first in my hierarchy in order to have a background image. If you do this, you have to manually set the edge insets of the scrollview in viewDidLayoutSubviews:
- (void) viewDidLayoutSubviews {
CGFloat top = self.topLayoutGuide.length;
CGFloat bottom = self.bottomLayoutGuide.length;
UIEdgeInsets newInsets = UIEdgeInsetsMake(top, 0, bottom, 0);
self.collectionView.contentInset = newInsets;
}
I found a better solution,use the undocumented method of UINavigationController.
#import <UIKit/UIKit.h>
#interface UINavigationController (ContentInset)
- (void) computeAndApplyScrollContentInsetDeltaForViewController:(UIViewController*) controller;
#end
#import "UINavigationController+ContentInset.h"
#interface UINavigationController()
- (void)_computeAndApplyScrollContentInsetDeltaForViewController:(id)arg1;
#end
#implementation UINavigationController (ContentInset)
- (void) computeAndApplyScrollContentInsetDeltaForViewController:(UIViewController*) controller
{
if ([UINavigationController instancesRespondToSelector:#selector(_computeAndApplyScrollContentInsetDeltaForViewController:)])
[self _computeAndApplyScrollContentInsetDeltaForViewController:controller];
}
#end
then,do like this
- (void) cycleFromViewController: (UIViewController*) oldC
toViewController: (UIViewController*) newC
{
[oldC willMoveToParentViewController:nil];
[self addChildViewController:newC];
[self transitionFromViewController: oldC toViewController: newC
duration: 0.25 options:0
animations:^{
newC.view.frame = oldC.view.frame;
[self.navigationController computeAndApplyScrollContentInsetDeltaForViewController:newC];
}
completion:^(BOOL finished) {
[oldC removeFromParentViewController];
[newC didMoveToParentViewController:self];
}];
}
Setting edgesForExtendedLayout = [] on my childcontrollers worked for me.
I'm trying to make a form that spans three tabs. You can see in the screenshot below where the tabs will be. When the user taps a tab, the Container View should update and show a particular view controller I have.
Tab 1 = View Controller 1
Tab 2 = View Controller 2
Tab 3 = View Controller 3
The view controller shown above has the class PPAddEntryViewController.m. I created an outlet for the Container view within this class and now have a Container View property:
#property (weak, nonatomic) IBOutlet UIView *container;
I also have my IBActions for my tabs ready:
- (IBAction)tab1:(id)sender {
//...
}
- (IBAction)tab2:(id)sender {
//...
}
- (IBAction)tab3:(id)sender {
//...
}
How do I set the container in those IBActions to change the view controller that the Container View holds?
Among a few other things, here's what I've tried:
UIViewController *viewController1 = [self.storyboard instantiateViewControllerWithIdentifier:#"vc1"];
_container.view = viewController1;
...but it doesn't work. Thanks in advance.
Switching using Storyboard, Auto-layout or not, a Button of some sort, and a series of Child View Controllers
You want to add the container view to your view and when the buttons that 'switch' child view controllers are pressed fire off the appropriate segue and perform the correct setup work.
In the Storyboard you can only connect one Embed Segue to the Container View. So you create an intermediate handling controller. Make the embed segue and give it an identifier, for example EmbededSegueIdentifier.
In your parent view controller wire up the button or whatever you want and keep are reference to your child view controller in the prepare segue. As soon as the parent view controller loads the segue will be fired.
The Parent View Controller
#property (weak, nonatomic) MyContainerViewController *myContainerViewController;
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"EmbeddedSegueIdentifier"]) {
self.myContainerViewController = segue.destinationViewController;
}
}
It should be fairly easy for you to delegate to your container controller the button presses.
The Container Controller
This next bit of code was partly borrowed from a couple of sources, but the key change is that auto layout is being used as opposed to explicit frames. There is nothing preventing you from simply changing out the lines [self addConstraintsForViewController:] for viewController.view.frame = self.view.bounds. In the Storyboard this Container View Controller doesn't do anything more that segue to the destination child view controllers.
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"%s", __PRETTY_FUNCTION__);
[self performSegueWithIdentifier:#"FirstViewControllerSegue" sender:nil];
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
UIViewController *destinationViewController = segue.destinationViewController;
if ([self.childViewControllers count] > 0) {
UIViewController *fromViewController = [self.childViewControllers firstObject];
[self swapFromViewController:fromViewController toViewController:destinationViewController];
} else {
[self initializeChildViewController:destinationViewController];
}
}
- (void)initializeChildViewController:(UIViewController *)viewController
{
[self addChildViewController:viewController];
[self.view addSubview:viewController.view];
[self addConstraintsForViewController:viewController];
[viewController didMoveToParentViewController:self];
}
- (void)swapFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController
{
[fromViewController willMoveToParentViewController:nil];
[self addChildViewController:toViewController];
[self transitionFromViewController:fromViewController toViewController:toViewController duration:0.2f options:UIViewAnimationOptionTransitionCrossDissolve animations:nil completion:^(BOOL finished) {
[self addConstraintsForViewController:toViewController];
[fromViewController removeFromParentViewController];
[toViewController didMoveToParentViewController:self];
}];
}
- (void)addConstraintsForViewController:(UIViewController *)viewController
{
UIView *containerView = self.view;
UIView *childView = viewController.view;
[childView setTranslatesAutoresizingMaskIntoConstraints:NO];
[containerView addSubview:childView];
NSDictionary *views = NSDictionaryOfVariableBindings(childView);
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"V:|[childView]|"
options:0
metrics:nil
views:views]];
[containerView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:#"H:|[childView]|"
options:0
metrics:nil
views:views]];
}
#pragma mark - Setters
- (void)setSelectedControl:(ViewControllerSelectionType)selectedControl
{
_selectedControl = selectedControl;
switch (self.selectedControl) {
case kFirstViewController:
[self performSegueWithIdentifier:#"FirstViewControllerSegue" sender:nil];
break;
case kSecondViewController:
[self performSegueWithIdentifier:#"SecondViewControllerSegue" sender:nil];
break;
default:
break;
}
}
The Custom Segues
The last thing you need is a custom segue that does nothing, going to each destination with the appropriate segue identifier that is called from the Container View Controller. If you don't put in an empty perform method the app will crash. Normally you could do some custom transition animation here.
#implementation SHCDummySegue
#interface SHCDummySegue : UIStoryboardSegue
#end
- (void)perform
{
// This space intentionally left blank
}
#end
I recently found the perfect sample code for what I was trying to do. It includes the Storyboard implementation and all the relevant segues and code. It was really helpful.
https://github.com/mhaddl/MHCustomTabBarController
Update: UITabBarController is the recommended way to go, as you found out earlier. In case you'd like to have a custom height, here is a good start: My way of customizing UITabBarController's tabbar - Stackoverflow answer
As of iOS 5+ you have access to customize the appearance via this API; UIAppearance Protocol Reference. Here is a nice tutorial for that: How To Customize Tab Bar Background and Appearance
The most obvious way to achieve what you're looking for is to simply manage 3 different containers (they are simple UIViews) and implement each of them to hold whatever content view you need for each tab (use the hidden property of the containers).
Here is an example of what's possible to achieve with different containers:
These containers "swapping" can be animated of course. About your self-answer, you probably chose the right way to do it.
have a member variable to hold the viewController:
UIViewController *selectedViewController;
now in the IBActions, switch that AND the view. e.g.
- (IBAction)tab1:(id)sender {
UIViewController *viewController1 = [self.storyboard instantiateViewControllerWithIdentifier:#"vc1"];
_container.view = viewController1.view;
selectedViewController = viewController1;
}
to fire view did appear and stuff call removeChildViewController, didMoveToParent, addChildViewController, didMoveToParent
I got this to work by using a UITabBarController. In order to use custom tabs, I had to subclass the TabBarController and add the buttons to the controller in code. I then listen for tap events on the buttons and set the selectedIndex for each tab.
It was pretty straight forward, but it's a lot of junk in my Storyboard for something as simple as 3 tabs.
I have ten different UIViewControllers with each containing a right and left button. The right arrow will bring the next controller in front while the left one will bring the previous one. I need to navigate between controllers without losing the last state. But every time, the right arrow is pressed, a new controller comes forward. I have tried pushController and presentController. I want to freely navigate between controllers without losing the state. Can you guide how to do it?
Questions like this are very broad, Cocoa Touch provides numerous mechanisms for managing view controllers. I suggest reading Apple's View Controller Programming Guide for iOS and in particular the Presenting View Controllers from Other View Controllers and Coordinating Efforts Between View Controllers sections.
That being said, here are two approaches to managing the flow of a number of UIViewControllers while keeping each alive so that all of their state (UI & underlying data) persist for the duration of their use. The key to both of these approaches is that a master view controller orchestrates the switching in and out of all of the UIViewControllers that need to be presented.
UINavigationController
Imagine your 10 view controllers 1 though 10. This approach is appropriate if the user is always presented view controller number 1 first and can only navigate sequentially forward and backwards through them i.e. the user cannot navigate between 1 and 10 directly:
A UINavigationController specialises in managing the navigation of hierarchical content. Although we may not think of our 10 view controllers as hierarchical conceptually, using a UINavigationController allows us to leverage its existing functionality for sequential navigation. In this model, each view controller is resposible for holding the reference to the following view controller in the sequence, presenting it and implementing its delegate callback to know when to dismiss it:
Code
MYPresentationViewController.h
#class MYPresentationViewController;
// The MYPresentationViewControllerDelegate allows the presenting view controller to know when the presented view controller (next highest in the presentation stack) has been dismissed.
#protocol MYPresentationViewControllerDelegate <NSObject>
- (void)viewControllerDidFinish:(MYPresentationViewController *)viewController;
#end
#interface MYPresentationViewController : UIViewController <MYPresentationViewControllerDelegate>
// The next view controller in the sequence.
#property (strong, nonatomic) MYPresentationViewController *nextViewController;
// The delegate responsible for showing and dismissing this view controller.
#property (assign) id<MYPresentationViewControllerDelegate> delegate;
#end
MYPresentationViewController.m
#import "MYPresentationViewController.h"
#implementation MYPresentationViewController
#pragma mark - Custom Property Assessors
// Returns the next view controller in the sequence. Creates and configures it if it doesn't already exist.
- (MYPresentationViewController *)nextViewController
{
if (_nextViewController == nil)
{
_nextViewController = [[MYPresentationViewController alloc] initWithNibName:#"MYPresentationViewController" bundle:nil];
_nextViewController.delegate = self;
}
return _nextViewController;
}
#pragma mark - UI Control Event Handlers
- (IBAction)leftButtonPressed:(UIButton *)sender
{
// Tell the delegate this view controller is ready to be dismissed.
[self.delegate viewControllerDidFinish:self];
}
- (IBAction)rightButtonPressed:(UIButton *)sender
{
// Present the next view controller.
[self.navigationController pushViewController:self.nextViewController animated:YES];
}
#pragma mark - MYPresentationViewControllerDelegate
- (void)viewControllerDidFinish:(MYPresentationViewController *)viewController
{
// Dismiss the next view controller to return to this one.
[self.navigationController popViewControllerAnimated:YES];
}
#end
Custom Master Controller
Imagine your 10 view controllers 1 though 10. This approach is appropriate if the user can be presented with a view controller that isn't at either end (1 or 10) first and/or the user can navigate between 1 and 10 directly allowing for a cyclical navigation:
Writing our own 'master controller' that overseers and facilites the switching in and out of all of the UIViewControllers that need to be presented allows us to implement the navigation in any way we want. In this model, the master view controller is responsible for handling the references to all of the view controllers being presented, presenting them and implementing their delegate callbacks:
Code
MYMasterViewController.h
#import "MYPresentationViewController.h"
#interface MYMasterViewController : UIViewController <MYPresentationViewControllerDelegate>
// The collection of view controllers to be presented.
#property (strong, nonatomic) NSArray *allViewControllers;
#end
MYMasterViewController.m
#import "MYMasterViewController.h"
#implementation MYMasterViewController
...
// Create 10 view controllers;
- (void)createViewControllers
{
NSMutableArray *allViewControllers = [NSMutableArray arrayWithCapacity:10];
// Create our 10 view controllers.
for (int i = 0; i < 10; i++)
{
MYPresentationViewController *viewController = [[MYPresentationViewController alloc] initWithNibName:#"MYPresentationViewController" bundle:nil];
viewController.delegate = self;
viewController.index = i;
[allViewControllers addObject:viewController];
}
self.allViewControllers = allViewControllers;
}
// Display first view controller, could be any in the sequence.
- (void)displayViewController
{
MYPresentationViewController *firstViewController = [self.allViewControllers objectAtIndex:0];
[self presentViewController:firstViewController animated:NO completion:nil];
}
#pragma mark - MYPresentationViewControllerDelegate
- (void)viewController:(MYPresentationViewController *)viewController didDismissWithButton:(kButtonPressed)button
{
[self dismissViewControllerAnimated:NO completion:nil];
// Determine the next view controller to display.
NSInteger index = viewController.index;
index += button == kButtonPressedLeft ? -1 : 1;
if (index < 0) {
index = self.allViewControllers.count - 1;
}
else if (index >= self.allViewControllers.count) {
index = 0;
}
MYPresentationViewController *nextViewController = [self.allViewControllers objectAtIndex:index];
[self presentViewController:nextViewController animated:NO completion:nil];
}
#end
MYPresentationViewController.h
#class MYPresentationViewController;
// Enumeration used to give a more descriptive code association with the presentation view controller's navigation button options.
typedef enum {
kButtonPressedLeft,
kButtonPressedRight,
} kButtonPressed;
// The MYPresentationViewControllerDelegate allows the presenting view controller to know when the presented view controller has been dismissed and with what button i.e. (left or right).
#protocol MYPresentationViewControllerDelegate <NSObject>
- (void)viewController:(MYPresentationViewController *)viewController didDismissWithButton:(kButtonPressed)button;
#end
#interface MYPresentationViewController : UIViewController
// The index of this view controller in relation to 10.
#property (assign) NSInteger index;
// The delegate responsible for showing and dismissing this view controller.
#property (assign) id<MYPresentationViewControllerDelegate> delegate;
#end
MYPresentationViewController.m
#import "MYPresentationViewController.h"
#implementation MYPresentationViewController
#pragma mark - UI Control Event Handlers
- (IBAction)leftButtonPressed:(UIButton *)sender
{
// Tell the delegate this view controller is ready to be dismissed.
[self.delegate viewController:self didDismissWithButton:kButtonPressedLeft];
}
- (IBAction)rightButtonPressed:(UIButton *)sender
{
// Tell the delegate this view controller is ready to be dismissed.
[self.delegate viewController:self didDismissWithButton:kButtonPressedRight];
}
#end
You need to maintain a stack of view controllers, similar to what UINavigationController does. When you move to the right, instead of creating a new view controller to present, you just get the next view controller in the array and present it
Then to maintain state each view controller should be responsible for managing its own state, and since you are not creating new controllers each time the state will be the same after a view has been removed and then presented again.
e.g. instead of
UIViewController *controller = //new view controller
[self presentViewController:controller];
you should do
UIViewController *controller = [self.controllers nextViewController];
[self presentNextController:controller];
This problem is driving me crazy. I'm trying to change the viewController when the user changes the selected "tab" of the segmented control. I've spent a couple hours researching and haven't been able to find an answer that works or is done through storyboard.
It really bother me since setting a tab application is so easy, but trying to use the segmented control like the tab application is just not working. I already know how to detect which index is selected in the segmented control. How can I achieve this?
Thank you very much.
NOTE: Answer updated with view controller containment code for iOS 5+ including #interface section
In an app of mine, I have a view controller with a Segment Control in the Navigation Bar and clicking on the "tabs" switches view controllers. The basic idea is to have an array of view controllers and switch between them using the Segment Index (and the indexDidChangeForSegmentedControl IBAction.
Example code (iOS 5 or later) from my app (this is for 2 view controllers but it's trivially extended to multiple view controllers); the code is slightly longer than for iOS 4 but will keep the object graph intact. Also, it uses ARC:
#interface MyViewController ()
// Segmented control to switch view controllers
#property (weak, nonatomic) IBOutlet UISegmentedControl *switchViewControllers;
// Array of view controllers to switch between
#property (nonatomic, copy) NSArray *allViewControllers;
// Currently selected view controller
#property (nonatomic, strong) UIViewController *currentViewController;
#end
#implementation UpdateScoreViewController
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
[super viewDidLoad];
// Create the score view controller
ViewControllerA *vcA = [self.storyboard instantiateViewControllerWithIdentifier:#"ViewControllerA"];
// Create the penalty view controller
ViewControllerB *vcB = [self.storyboard instantiateViewControllerWithIdentifier:#"ViewControllerB"];
// Add A and B view controllers to the array
self.allViewControllers = [[NSArray alloc] initWithObjects:vcA, vcB, nil];
// Ensure a view controller is loaded
self.switchViewControllers.selectedSegmentIndex = 0;
[self cycleFromViewController:self.currentViewController toViewController:[self.allViewControllers objectAtIndex:self.switchViewControllers.selectedSegmentIndex]];
}
#pragma mark - View controller switching and saving
- (void)cycleFromViewController:(UIViewController*)oldVC toViewController:(UIViewController*)newVC {
// Do nothing if we are attempting to swap to the same view controller
if (newVC == oldVC) return;
// Check the newVC is non-nil otherwise expect a crash: NSInvalidArgumentException
if (newVC) {
// Set the new view controller frame (in this case to be the size of the available screen bounds)
// Calulate any other frame animations here (e.g. for the oldVC)
newVC.view.frame = CGRectMake(CGRectGetMinX(self.view.bounds), CGRectGetMinY(self.view.bounds), CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds));
// Check the oldVC is non-nil otherwise expect a crash: NSInvalidArgumentException
if (oldVC) {
// Start both the view controller transitions
[oldVC willMoveToParentViewController:nil];
[self addChildViewController:newVC];
// Swap the view controllers
// No frame animations in this code but these would go in the animations block
[self transitionFromViewController:oldVC
toViewController:newVC
duration:0.25
options:UIViewAnimationOptionLayoutSubviews
animations:^{}
completion:^(BOOL finished) {
// Finish both the view controller transitions
[oldVC removeFromParentViewController];
[newVC didMoveToParentViewController:self];
// Store a reference to the current controller
self.currentViewController = newVC;
}];
} else {
// Otherwise we are adding a view controller for the first time
// Start the view controller transition
[self addChildViewController:newVC];
// Add the new view controller view to the ciew hierarchy
[self.view addSubview:newVC.view];
// End the view controller transition
[newVC didMoveToParentViewController:self];
// Store a reference to the current controller
self.currentViewController = newVC;
}
}
}
- (IBAction)indexDidChangeForSegmentedControl:(UISegmentedControl *)sender {
NSUInteger index = sender.selectedSegmentIndex;
if (UISegmentedControlNoSegment != index) {
UIViewController *incomingViewController = [self.allViewControllers objectAtIndex:index];
[self cycleFromViewController:self.currentViewController toViewController:incomingViewController];
}
}
#end
Original example (iOS 4 or before):
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
[super viewDidLoad];
// Create the score view controller
AddHandScoreViewController *score = [self.storyboard instantiateViewControllerWithIdentifier:#"AddHandScore"];
// Create the penalty view controller
AddHandPenaltyViewController *penalty = [self.storyboard instantiateViewControllerWithIdentifier:#"AddHandPenalty"];
// Add Score and Penalty view controllers to the array
self.allViewControllers = [[NSArray alloc] initWithObjects:score, penalty, nil];
// Ensure the Score controller is loaded
self.switchViewControllers.selectedSegmentIndex = 0;
[self switchToController:[self.allViewControllers objectAtIndex:self.switchViewControllers.selectedSegmentIndex]];
}
#pragma mark - View controller switching and saving
- (void)switchToController:(UIViewController *)newVC
{
if (newVC) {
// Do nothing if we are in the same controller
if (newVC == self.currentViewController) return;
// Remove the current controller if we are loaded and shown
if([self.currentViewController isViewLoaded]) [self.currentViewController.view removeFromSuperview];
// Resize the new view controller
newVC.view.frame = CGRectMake(CGRectGetMinX(self.view.bounds), CGRectGetMinY(self.view.bounds), CGRectGetWidth(self.view.bounds), CGRectGetHeight(self.view.bounds));
// Add the new controller
[self.view addSubview:newVC.view];
// Store a reference to the current controller
self.currentViewController = newVC;
}
}
- (IBAction)indexDidChangeForSegmentedControl:(UISegmentedControl *)sender {
NSUInteger index = sender.selectedSegmentIndex;
if (UISegmentedControlNoSegment != index) {
UIViewController *incomingViewController = [self.allViewControllers objectAtIndex:index];
[self switchToController:incomingViewController];
}
}
I'd say it's much simpler to change subviews within a UIViewController, you can set up your subviews in your storyboards and hook them up with IBOulets in your controller you can set the hidden property of your views to YES or NO depending on the control that was clicked.
Now, if you use #Robotic Cat's approach which is also a good solution you can have a little more modularity in how your app works, considering you'd have to place all your logic in one controller using the solution I presented.
UISegmentedControl is a little different in that it doesn't have a delegate protocol, you have to use the "add target" style. In your case what you want to do is add a target to be notified when the UISegmentedControl changes (which is likely the parent view controller), and then that target can deal with the tab switching.
For example:
[self.mainSegmentedControl addTarget:self action:#selector(changedSegmentedControl:) forControlEvents:UIControlEventValueChanged];
In this example, the code is being invoked from some view/controller that has access to the variable for the segmented control. We add ourself to get the changedSegmentedControl: method invoked.
Then you would have another method like so:
- (void)changedSegmentedControl:(id)sender
{
UISegmentedControl *ctl = sender;
NSLog(#"Changed value of segmented control to %d", ctl.selectedSegmentIndex);
// Code to change View Controller goes here
}
Note: this is untested code written from memory -- please consult the docs accordingly.
Take a look at this pod: https://github.com/xmartlabs/XLMailBoxContainer. It makes the UI animation among the view controllers. These view controller can extend UITableViewController or any other view controller.
I hope this help you!