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];
Related
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!
I was wondering how I would be able to update a view controller property of the presenting view controller from a method called inside the modal view controller? Right now, the only way I could think of is using NSNotificationCenter, and while this works for Dictionary items, I couldn't figure out how to use it for a custom object.
For example, my presenting view controller HomewViewController has a Parse PFObject called homeSelections, which the could be updated in the modally presented ModalViewController's property newSelections (and also a PFObject) . After the user makes her selections, I would like HomeViewController's homeSelections to also have the latest data passed from the modal view controller.
Help is appreciated - thanks.
Update 1: here is what I have done now (note that I am using a stripped down example to test things out)
In ViewController (this is the parent/presenting view controller)
#interface ViewController ()
#property (strong, nonatomic) NSArray *totalRamen;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.totalRamen = #[#"ramen1", #"ramen2", #"ramen3", #"moot"];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(#"self.totalRamen: %#", self.totalRamen);
NSLog(#"Done");
}
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"showModal"]){
ModalViewController *destinationVC = (ModalViewController *)segue.destinationViewController;
destinationVC.passedRamen = self.totalRamen;
}
}
- (IBAction)showModalAction:(UIButton *)sender
{
ModalViewController *destinaionViewController = [[ModalViewController alloc] init];
destinaionViewController.selectionCallback = ^(id selectedItem) {
self.totalRamen = (NSArray *)selectedItem;
NSLog(#"self.totalRAmen %#", self.totalRamen);
NSLog(#"done");
};
[self performSegueWithIdentifier:#"showModal" sender:self];
}
In ModalViewController (this is the presented/modal view controller)
#interface ModalViewController : UIViewController
#property (strong, nonatomic) NSArray *passedRamen;
- (IBAction)dismissModal:(UIButton *)sender;
typedef void(^CallbackBlock)(id value);
#property (nonatomic, copy) CallbackBlock selectionCallback;
#end
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
NSLog(#"Passed ramen %#", self.passedRamen);
NSLog(#"Done");
self.passedRamen = #[#"moot is awesome"];
NSLog(#"new ramen: %#", self.passedRamen);
NSLog(#"%#", self.selectionCallback); //nil here
//call back
CallbackBlock selectionCallback = self.selectionCallback;
if (selectionCallback){
selectionCallback(self.passedRamen); //I want to send the newly updated self.passedRamen back
} else {
NSLog(#"No show"); //Means if isn't called
}
}
- (IBAction)dismissModal:(UIButton *)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
Pass a callback block into the presented view controller. This way the presented view controller doesn't know anything about the view controller presenting it. Much more flexible because now anybody can present your view controller, they just pass it a block!
PresentingViewController
PresentedViewController *vc = [[PresentedViewController alloc] init]; //or get your existing one
vc.selectionCallback = ^(id selectedItem) {
//update selected items here
};
//present vc here
PresentedViewController
typedef void(^CallbackBlock)(id value);
#property (nonatomic, copy) CallbackBlock selectionCallback;
- (void)somethingWasSelected:(id)selectedItem {
CallbackBlock selectionCallback = self.selectionCallback;
if (selectionCallback) selectionCallback(selectedItem);
}
Beware of retain cycles. This block is being retained by the presented view controller so references to the presented view controller in the block without weakifying it first will create a leak. More info on this can be found here.
After the user makes her selections, I would like HomeViewController's
homeSelections to also have the latest data passed from the modal view
controller.
The easy way is to avoid having ModalViewController update HomeViewController at all. Turn the communication around -- have HomeViewController query ModalViewController and update itself when the modal is dismissed.
HomeViewController already depends on ModalViewController -- it has to know about ModalViewController in order to present it. So there's no harm in having it also know how to read the newSelections property out of ModalViewController at the appropriate time. ModalViewController, on the other hand, has no need to know anything about where its information goes. It doesn't need to know about HomeViewController in order to do its job. If you avoid telling ModalViewController anything about HomeViewController, you can easily use ModalViewController from some other view controller should the need for that ever arise. More importantly, you avoid ever needing to update ModalViewController if HomeViewController changes.
For example, your HomeViewController might look (in part) like this:
- (void)showModalViewController
{
self.modalViewController = [[ModalViewController alloc] init]; // or otherwise get the modal controller
[self presentViewController:self.modalViewController
animated:YES
completion:];
}
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion
{
self.homeSelections = self.modalViewController.newSelections;
[super dismissViewControllerAnimated:flag completion:completion];
self.modalViewController = nil;
}
I'm new to iOS programming and I'm facing a problem
I'm having a problem with custom delegate.
I'm trying to make a simple custom where it return data to the previous view controller and pop the current view controller.
I have 2 navigation view controller
1 - main view controller
2 - Adding
and here is the protocol that is written in the adding view controller
#protocol AddingDelegate <NSObject>
#required
-(void)setInformation:(Adding *)controller withObject:(Conference *)info;
and here is the where I called it in adding view controller
-(IBAction)addingConference
{
NSLog(#"Adding Button Pressed");
conferenceObject = [[Conference alloc]init];
conferenceObject.name = [NameTX text];
conferenceObject.city = [CityTX text];
conferenceObject.description = [Dectription text];
NSMutableArray *info = [[NSMutableArray alloc] init];
[info addObject:conferenceObject];
[self.delegate setInformation:self withObject:conferenceObject];
NSLog(#"adding Conference method is done");
}
I wrote the delegate at the interface in the main view controller
#interface MainViewController : UITableViewController <AddingDelegate>
#end
and here where I declared the delegate method
-(void)setInformation:(Adding *)controller withArray:(NSMutableArray *)info
{
NSLog(#"in the main view at the delegate");
[self.navigationController popToRootViewControllerAnimated:YES];
NSLog(#"Should be popped right now");
}
and this is the prepare for segue method
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([segue.identifier isEqualToString:#"AddObject"]) {
UINavigationController *navigation = segue.destinationViewController;
Adding *addingViewController = [[navigation viewControllers]objectAtIndex:0];
addingViewController.delegate = self;
}
}
now the problem is when I push the adding on top of the stack and then fill the information and press done the adding view controller doesn't pop to show main view controller.
I tried to log everything and the logs from the main view controller doesn't show .
Please help me
What I notice here is that in the implementation of prepareForSegue:sender: the segue's destinationViewController is a navigation controller. This makes me think that your segue is not pushing the AddingController on the current navigation stack but it's presenting a new one instead. This means the new navigation controller containing the AddingController is presented modally and as such, when you try to pop the navigation stack nothing seems to happen because you're operating on the wrong navigation stack. If that is the case you have two options: 1. change [self.navigationController popToRootViewControllerAnimated:YES]; for [self dismissViewControllerAnimated:YES completion:nil]; or 2. change the segue to be a push segue instead of a modal segue and point the segue directly to the AddingController.
In Adding.m
#class Adding;
#protocol AddingDelegate
- (void)flipsideViewControllerDidFinish:(FlipsideViewController *)controller;
#end
#interface Adding : UIViewController
#property (weak, nonatomic) id <AddingDelegate> delegate; // have you forgot this one
#end
and use
[self dismissViewControllerAnimated:YES completion:nil];
You need dismiss if you want get back to previous screen and make sure you have added Navigation controller
I am presenting a modal view using a storyboard segue set as Form Sheet.
The problem is, when I rotate the iPad after this view is displayed, the view is removed from the view/dismissed.
I have no idea why. It only seems to occur when starting in Portrait then rotating to Landscape.
If I start in Landscape then show the view then rotate it stays on the screen fine.
Any ideas?
EDIT ----
It also seems that full screen modal views are also dismissed after rotation!
There's nothing special going on in the presentation code, this is a full screen modal:
EditViewController *editView = [self.navigationController.storyboard instantiateViewControllerWithIdentifier:#"editViewController"];
editView.delegate = self;
editView.image = image;
editView.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
[self presentViewController:editView animated:YES completion:nil];
This happens on both iOS 6 and iOS 7
EDIT 2 ----
Forgot to mention, i'm presenting the modal from the left/master view controller of a UISplitViewController
late, but what it worked for me was just before
[self presentViewController:aController animated:YES completion:nil];
dismiss the master controller, adding this lines
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModePrimaryHidden];
[self.splitViewController setPreferredDisplayMode:UISplitViewControllerDisplayModeAutomatic];
and then present your controller
Get rid of: editView.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
That will solve the issue you are experiencing. Modal view controllers presented on iPad as a Form Sheet do not rotate correctly using that transition style.
its really hard to get the cause how and why this is happening as i found that this also happen with UIPopover also as when you rotate it UIPopover hide because ???
So if you want to keep your view then just call again your controller after rotation will do fine user experience
This is not a bug its a limitation on UISplitViewController. The problem exists when the masterViewController (which is a UIPopoverController) is able to be dismissed. Heres how it works with the assumption that your app does allow the masterViewController to be dismissed in portrait and does not allow in landscape.
In portrait while the masterViewController is visible, if you were to present a modal from a viewController in the masterViewController and then rotate to landscape, the modal would disappear in iOS7 and the app would not rotate in iOS8. iOS8 introduces a condition to prevent the bad experience of iOS7. iOS7 losses the modal in the process of moving the masterViewController from the popoverController to a contained viewController in the splitViewController.
The modal needs to be presented from the splitViewController and not from the masterViewController. The only problem with this is the modal gets presented below the masterViewController in portrait. My solution is to dismiss the masterViewController and then present the modal.
There are several ways to achieve this result depending on how complex your code needs to be. Here's how I do this in my app.
I first subclass UISplitViewController in order to have a reference to the popoverController. I use delegate forwarding in order to access the delegate methods internally and externally. Heres the .h
// MainSplitViewController.h
#import <UIKit/UIKit.h>
#interface MainSplitViewController : UISplitViewController
#property (nonatomic, weak, readonly) UIPopoverController* primaryColumnController;
#end
And the .m
// MainSplitViewController.m
#import "MainSplitViewController.h"
#interface MainSplitViewController () <UISplitViewControllerDelegate>
#property (nonatomic, weak) id<UISplitViewControllerDelegate> externalDelegate;
#property (nonatomic, weak) UIPopoverController* primaryColumnController;
#end
#implementation MainSplitViewController
- (instancetype)init {
self = [super init];
if (self) {
self.delegate = self;
}
return self;
}
#pragma mark - Split View Controller Delegate
- (void)splitViewController:(UISplitViewController *)svc popoverController:(UIPopoverController *)pc willPresentViewController:(UIViewController *)aViewController {
self.primaryColumnController = pc;
if ([(id)self.externalDelegate respondsToSelector:_cmd]) {
[self.externalDelegate splitViewController:svc popoverController:pc willPresentViewController:aViewController];
}
}
#pragma mark - Delegate Forwarder
- (void)setDelegate:(id<UISplitViewControllerDelegate>)delegate {
[super setDelegate:nil];
self.externalDelegate = (delegate != self) ? delegate : nil;
[super setDelegate:delegate ? self : nil];
}
- (BOOL)respondsToSelector:(SEL)aSelector {
id delegate = self.externalDelegate;
return [super respondsToSelector:aSelector] || [delegate respondsToSelector:aSelector];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
id delegate = self.externalDelegate;
return [delegate respondsToSelector:aSelector] ? delegate : [super forwardingTargetForSelector:aSelector];
}
#end
Next I create a class extension on UIViewController
// UIViewController+Popover.h
#import <UIKit/UIKit.h>
#interface UIViewController (Popover)
- (UIViewController *)popoverPresentingViewController;
#end
And the .m
// UIViewController+Popover.m
#import "UIViewController+Popover.h"
#import "MainSplitViewController.h"
#implementation UIViewController (Popover)
- (UIViewController *)popoverPresentingViewController {
UIViewController* viewController = self;
if ([self.splitViewController isKindOfClass:[MainSplitViewController class]]) {
viewController = self.splitViewController;
MainSplitViewController* mainSplitViewController = (MainSplitViewController *)self.splitViewController;
if (mainSplitViewController.primaryColumnController.popoverVisible) {
[mainSplitViewController.primaryColumnController dismissPopoverAnimated:YES];
}
}
return viewController;
}
#end
Now where ever you present the modal, instead of calling [self presentViewController: ... call [self.popoverPresentingViewController presentViewController: ...]. Remember to import UIViewController+Popover.h
your question came closest to my bug, On returning from modalView the parentView will switch to orientation in which the application was opened.
Visually it appears that the modal view is rotated and then returns.
I solved it by removing the modal view altogether, and using
[self.navigationController pushViewController: <the View(not modal now)>]
instead of using-
[self presentViewController:<Modal View>]
I think this is because the navigation controller doesn't own the Modal View, hence it reloads - when returning from the modal view - to incorrect orientation
Problem:
When presenting a view controller modally, it gets dismissed on rotation.
Approach:
Set the UISplitViewControllerDelegate
Use the UISplitViewControllerDelegate methods
Hold a reference to your modal view controller in an instance variable
Check if your modal view controller's presenting view controller exists.
If it exists, nothing needs to be done, else just present without any animation.
UISplitViewControllerDelegate methods:
func primaryViewController(forCollapsing splitViewController: UISplitViewController) -> UIViewController? {
if let someModalViewController = someModalViewController,
someModalViewController.presentingViewController == nil {
let masterViewController = viewControllers.first
masterViewController?.present(someModalViewController,
animated: false) {
}
}
return nil
}
func primaryViewController(forExpanding splitViewController: UISplitViewController) -> UIViewController? {
if let someModalViewController = someModalViewController,
someModalViewController.presentingViewController == nil {
let masterViewController = viewControllers.first
masterViewController?.present(someModalViewController,
animated: false) {
}
}
return nil
}
Note:
UISplitViewControllerDelegate has quite a methods, it can be daunting initially, if you spend some time experimenting, you can achieve what you want.
It has fine grained access.
I'm very late but try this. It works for me.
[self.splitViewController presentViewController:editView animated:YES completion:nil];
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!