I want to create a custom segue that acts in the same way as the standard push segue does when used on UINavigationController view controllers. I've implemented my custom segue:
CustomSegue.m
-(void)perform {
UIViewController *source = (UIViewController *)[self sourceViewController];
UIViewController *dest =(UIViewController *)[self destinationViewController];
if (1==2 //test) {
[source.navigationController pushViewController:dest animated:YES];
}
else {
UIViewController *altDest = [[UIStoryboard storyboardWithName:#"MainStoryboard" bundle:NULL]
instantiateViewControllerWithIdentifier:#"alternateView"];
[source.navigationController pushViewController:altDest animated:YES];
}
As you can see, the reason I want to use a custom push segue is so that I can decide which view controller to push based on the user configuration (currently only checking a trivial 1==2 expression). I can instantiate the alternate view controller with no issue, but what I want to be able to do is go back and forth without reloading the view controller each time (using the back and next buttons). Is there a way to retrieve an existing instance from the storyboard, or some standard way of doing this?
Instead of a custom segue with its perform, the way to do what you describe, i.e. choose in real time whether to push dest or altDest, is either (1) do not use segues at all and just call pushViewController directly as you are doing here, or (2) prepare two segues emanating from the view controller as a whole, and call performSegueWithIdentifier: to say which we should perform.
As for going directly from dest to altDest, you can push altDest on top of dest and then remove dest from the stack of the navigation controller's view controllers.
Like so much about about iOS, this is all so much easier and more obvious if you do not use a storyboard at all. This is why I don't like storyboards: they are so simple-minded and limiting, and they distract one's attention from the way iOS really works.
There is no way to retrieve an existing controller from a storyboard -- it would be nice if there were a controllerWithIdentifier: method to do that, but there isn't. Segues (other than unwinds) always instantiate new controllers, so I don't think you can do what you want with a segue. If you want to be going forward (push) to the same controller multiple times, then you need to do it in code by creating a property that points to your controller, and checking if that controller exists before pushing to it.
As the others have pointed out, you can't use a segue to push to an existing instance of a controller. The process of performing a segue always creates a new instance the destination controller for you.
Personally, when I'm jumping between existing instances of view controllers, I think "container view controller", such as a UIPageViewController, which makes it really easy to transition between two or more controllers, without necessarily reinstantiating them every time.
If you don't like the constraints the page view controller imposes (e.g. maybe you don't like the fact that iOS 5 version only supports page curl transitions, or that iOS 6 only adds the scroll transition, and you want something else), then you'd do a custom container view controller.
For example, if I wanted to jump between two view controllers and not reinstantiate them every time, I'd first create a custom container view controller, the "parent", and make sure I have a property to keep track of which child I'm currently at:
#property (nonatomic) NSInteger childViewIndex;
If supporting iOS 6.0 and above only, I'd then add a "container view" to my parent view controller's scene. If supporting iOS versions prior to 6.0, I'd add a standard UIView to the scene and then manually instantiate the first child controller:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
UIViewController *controller;
// add the first child
UIViewController *controller = [self addChildWithIdentifier:#"One"];
[self.containerView addSubview:controller.view];
[controller didMoveToParentViewController:self];
self.childViewIndex = 0;
}
- (UIViewController *)addChildWithIdentifier:(NSString *)storyboardIdentifier
{
UIViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:storyboardIdentifier];
[self addChildViewController:controller];
controller.view.frame = self.containerView.bounds;
controller.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
return controller;
}
Then, when I want to transition to the second child (or transition back to the first child), I'd call the following routine in the parent view controller:
- (void)transitionToViewControllerIndex:(NSInteger)index
{
// don't do anything if we're trying to transition to ourselves!
if (index == self.childViewIndex)
return;
// identify the two controllers in question
UIViewController *sourceController = self.childViewControllers[self.childViewIndex];
UIViewController *destinationController;
// if we're asking for page 2, but we only have one defined, then we'll have to instantiate it
BOOL instantiateDestination = (index == 1 && [self.childViewControllers count] < 2);
if (instantiateDestination)
destinationController = [self addChildWithIdentifier:#"Two"];
else
destinationController = self.childViewControllers[index];
// configure the destination controller's frame
destinationController.view.frame = sourceController.view.frame;
// if you're jumping back and forth, set the animation appropriate for the
// direction we're going
UIViewAnimationOptions options;
if (index > self.childViewIndex)
{
options = UIViewAnimationOptionTransitionFlipFromRight;
}
else
{
options = UIViewAnimationOptionTransitionFlipFromLeft;
}
// now transition to that destination controller
[self transitionFromViewController:sourceController
toViewController:destinationController
duration:0.5
options:options
animations:^{
// for simple flip, you don't need anything here,
// but docs say this can't be NULL; if you wanted
// to do some other, custom annotation, you'd do it here
}
completion:^(BOOL finished) {
if (instantiateDestination)
[destinationController didMoveToParentViewController:self];
}];
self.childViewIndex = index;
}
Thus, to transition to the second child view controller, you could simply call:
[self transitionToViewControllerIndex:1];
If you want to transition back, you could call:
[self transitionToViewControllerIndex:0];
I'm only scratching the surface here, but container view controllers (or if none of the standard ones do the job for you, a custom container view controller) is precisely what you need.
For more information, see:
Creating Custom Container View Controllers in the View Controller Programming Guide for iOS.
Implementing UIViewController Containment in the WWDC 2011 Session Videos (Apple developer ID required).
Implementing a Container View Controller in the UIViewController Class Reference.
Page View Controllers in the View Controller Catalog for iOS.
Related
I'm quite new to iOS development and I am stuck. Currently I am using one tab controller to switch between two view controllers (list and map view). This made it easier to use storyboard to configure the look of the two views.
Now the requirements have changed and the app needs to have one view controller with a segmented control that on click, displays either the list or the map view. In order to do this, I would need to make one view controller that can display list/map view.
I understand how the segmented controller part works, but I'm just stuck on how I can go about having two views with one or the other displayed in the same area.
How can I go about having two views in one view controller (if possible, utilizing storyboard)?
Thanks in advance!
You should not have two main views in a single view controller, instead you need to create one view controller per view that you want to show. However you can certainly have multiple subviews in a single view controller, which may be what works for you.
There are a number of approaches to solve this the problem, the correct approach would be to create a container UIViewController, and add as its childs the 2 viewcontrollers you want to show, them simply set the view to the view controller you want to display, but that would probably be overly complicated since you mention you are new to iOS development.
Therefore an easy solution (not sure if you can implement this in storyboard - since I don't like it), would be to have a single view controller, with the tabs, and 2 subviews of the main view, then you can simply switch between views by doing something like this:
[self.view addSubview:view1];
//to switch
[view1 removeFromSuperview];
[self.view addSubView:view2];
alternatively, you do not really need to remove it from superview but just hide it, and then use bringSubViewToFront to show the view that you need.
If you want to use the other approach I would recommend looking for this video the WWDC 2011 video titled "Implementing UIViewController Containment". This other question should be useful to: UISegmented control with 2 views
Hope that helps.
Using storyboard api you can switch between 2 viewControllers
- (void)viewDidLoad {
[super viewDidLoad];
UIViewController *viewController = [self viewControllerForSegmentIndex:self.typeSegmentedControl.selectedSegmentIndex];
[self addChildViewController:viewController];
viewController.view.frame = self.contentView.bounds;
[self.contentView addSubview:viewController.view];
self.currentViewController = viewController;
}
- (UIViewController *)viewControllerForSegmentIndex:(NSInteger)index {
UIViewController *viewController;
switch (index) {
case 0:
viewController = [self.storyboard instantiateViewControllerWithIdentifier:#"FirstViewController"];
break;
case 1:
viewController = [self.storyboard instantiateViewControllerWithIdentifier:#"SecondViewController"];
break;
}
return viewController;
}
- (IBAction)segmentChanged:(UISegmentedControl *)sender {
UIViewController *viewController = [self viewControllerForSegmentIndex:sender.selectedSegmentIndex];
[self addChildViewController:viewController];
[self transitionFromViewController:self.currentViewController toViewController:viewController duration:0.0 options:UIViewAnimationOptionTransitionNone animations:^{
[self.currentViewController.view removeFromSuperview];
viewController.view.frame = self.contentView.bounds;
[self.contentView addSubview:viewController.view];
} completion:^(BOOL finished) {
[viewController didMoveToParentViewController:self];
[self.currentViewController removeFromParentViewController];
self.currentViewController = viewController ;
}];
self.navigationItem.title = viewController.title;
}
This is in reference to iOS tutorial by Raywenderlich. Hope this helps
With Storyboard it is possible in this way.
Create UIViewController with UISegmentControl and UITableView+UITableViewCell added to it.
Now you want to add MKMapView as well, hoverer, if you simply try to place the MapView on the ViewController, it will be added as new TableView cell, which is not what we want.
That's why you should not do it so. Instead, MapView has to be added to Storyboard's List of ViewControllers
Adjust the size and origin of MapView to be the same as TableView ones.
Now, setHidden to YES for either TableView of MapView, create and synthesize outlets for them. Then in Segment control Value Changed method implement switching:
- (IBAction)switchView:(id)sender {
self.theTableView.hidden = !self.theTableView.hidden;
self.theMapView.hidden = !self.theMapView.hidden;
if (!self.theTableView.hidden) {
[self.theTableView reloadData];
}
}
I need some help in picking the 'right' solution for the following 'problem'.
I use the new storyboard feature to link all the screens of my application together. Basically the structure drills down to:
[Navigation Controller] => [View Controller #1] => [Tabbar Controller] => [View Controller #2]*
**(and some other tabs which are for now not important)*
I have attached a segue (push) from the first View Controller (#1) to the View Controller behind the Tab Bar Controller. This push is triggered when the users presses something on the first controller and works fine.
// Execute preset segue
[self performSegueWithIdentifier:#"segueEventDetail" sender:self];
When the user (which is now in the View Controller #2) presses the back button in the navbar the user goes back. Suppose he now triggers the segue again, the second view controller is shown again but is now 'resetted' (empty). (I believe after reading several fora and articles this is standard behavior when using segue's because these destroy and reinitiliaze the view controller's every time?)
This (the view controller being resetted) poses a problem because the contents of the second view controller is dynamic (depend on a JSON response from the server) and thus it is 'needed' that the view controller remains intact (or is restored) when the user comes back.
I have found several sources (see bottom) describing the same issue, but the solutions vary and I need some help picking the right one.
Summarize:
How can I 'retain'/save the state of a View Controller when the users presses back, while preserving the use of Storyboard & preferably also Segue's
Own Thoughts:
#1 I'm now thinking of caching the JSON Response to my singleton class (and from there to a PLIST) and checking within the second view controller if this data is present and than rebuild the view after which I check for any new data (resume normal operation).
#2 Another one I'm thinking of is 'bypassing' the segue and manually handle the switch of views , partially explained in (Storyboard - refer to ViewController in AppDelegate) - Is this also possible?
But maybe there is an easier/better option?
http://www.iphonedevsdk.com/forum/iphone-sdk-development/93913-retaining-data-when-using-storyboards.html
Storyboard - refer to ViewController in AppDelegate
How to serialize a UIView?
Yess!! I got the solution. Do the following:
In you're .h file:
#property (strong, nonatomic) UITabBarController *tabController;
In you're .m file:
#synthesize tabController;
tabController = [self.storyboard instantiateViewControllerWithIdentifier:#"tabbar"];
The selected index is the tab you want to go
tabController.selectedIndex = 1;
[[self navigationController] pushViewController:tabController animated:YES];
For anyone coming across this (my) question in the future, this is how I ended up 'coding' it.
Open the storyboard and select your 'Tab Bar Controller' and open the Attributes Inspector
Fill in an 'identifier' in the field
With the first view controller (see scenario in original post) I create an global reference to the viewcontroller:
firstviewcontroller.h
#interface YourViewController : UIViewController {
UITabBarController *tabController;
}
firstviewcontroller.m
//Fill the reference to the tabcontroller using the identifier
tabController = [self.storyboard instantiateViewControllerWithIdentifier:#"tabbar"];
Now to switch from the firstviewcontroller the following line can be used:
[[self navigationController] pushViewController:tabController animated:YES];
This might be even more simple solution (without using properties - in fact, all your class instances don't need to know about their destination controllers, so just save it as static in the pushing function):
static UIVewController *destController = nil;
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:#"MainStoryboard" bundle:nil];
if (!storyboard) {
DLog(Storyboard not found);
return;
}
if (destController == nil) { //first initialisation of destController
destController = [storyboard instantiateViewControllerWithIdentifier:#"{Your Destination controller identifyer}"];
if(!destController) {
DLog(destController not found)
return;
}
}
//set any additional destController's properties;
[self.navigationController pushViewController:destController animated:YES];
p.s. DLog is just my variation of NSLog.
But it's really interesting how to do this with segue?
I'm trying to switch between several table views as the root of a navigation controller. Depending on the settings of my app, I want to use different sets of data with different methods, and prefer to have these encapsulated in separate classes.
My thought was to set a view manager class (UIViewController) as the root view controller of the navigation controller. In the view manager we check the settings to see which view we want to load:
if([application_mode intValue]==APPLICATION_MODE_A){
AViewController *aView = [[DeviceTableViewController alloc] init];
[self.view insertSubview:aView.view atIndex:0];
}
else if([application_mode intValue]==APPLICATION_B){
BViewController *bView = [[BViewController alloc] init];
[self.view insertSubview.bView.view atIndex:0];
}
That does in fact insert the appropriate view into the view manager, at the cost of a white bar at the top of the inserted view and no info on the navigation bar, ie the subview is not connected to the navigation controller.
What's the proper way to do this? I'd really prefer not to have one ginormous table view!
Where do you set your navigationController's rootViewController? Can't you just set it to an AviewController's object or an BViewController's object at this time ? You may not need an intermediate UIViewController
I would do at the beginning :
//navigationController comes from a Xib or previous code
if([application_mode intValue]==APPLICATION_MODE_A){
AViewController *aView = [[DeviceTableViewController alloc] init];
navigationController.rootViewController = aView;
[aView release];
}
else if([application_mode intValue]==APPLICATION_B){
BViewController *bView = [[BViewController alloc] init];
navigationController.rootViewController = bView;
[bView release];
}
Since there is no view controller containment, I like the approach outlined in Jonah William's blog:
http://blog.carbonfive.com/2011/03/09/abusing-uiviewcontrollers/
You can't effectively place a view controller inside another; instead, we create something with similar lifecycle methods (viewDidLoad, viewDidAppear, etc) and forward those methods from the parent to the child. This 'psudo-viewcontroller' has a view property that we add as a subview to the parent's view, using UIView addSubView
http://developer.apple.com/library/ios/#documentation/uikit/reference/UIView_Class/UIView/UIView.html#//apple_ref/occ/instm/UIView/addSubview:
With this approach, we can encapsulate view elements, switch them out dynamically in a view controller, place several within a single view controller, etc. This way they can be considered separately from your navigation stack. It's a bit of work, but the cleanest UI encapsulation approach in iOS 4 in my opinion.
I have a UIViewController that is creating another view controller, and adding its view as a subview:
In the parent UIViewController:
SlateMoreView* subView = [[SlateMoreView alloc] initWithNibName:#"SlateMoreView" bundle:nil];
[self.view addSubview:subView.view];
I then need to call a method from the subview, in the parent view.
I have seen how to do this when I am adding the sub UIViewController using [self.navigationController pushViewController: subView animated: YES], because I can find the parent using this kind of code:
In the sub view UIViewController:
NSArray* viewControllerArray = [self.navigationController viewControllers]
int parentViewControllerIndex = [viewControllerArray count] - 2;
SlateView* slateView = [viewControllerArray objectAtIndex:parentViewControllerIndex];
...and then I can send messages to it. But since I added the sub view manually by using addSubView, I can't do this.
Can anyone think of how I can talk to my parent UIViewController?
Thanks!
UIViews have a superview property which seems to be what you are looking for.
In addition you probably don't want to nest UIViewController's view like that unless you are very deliberately building a custom contain view controller. See http://blog.carbonfive.com/2011/03/09/abusing-uiviewcontrollers/
You might want to consider if your problem can be solved by using NSNotifications. You could post a notification from your subview when an event happens that interested listeners (your superview) need to know about . When the superview receives the notification, it can run whatever code you wish. All the while the subview never needs to know about the superview.
This is one way to make your classes less dependant on each other.
You could also use delegation as another option.
When you add your view as a subview to a view hierarchy, you put it in the responder chain. You can go up the responder chain to reach the view controller as a UIView controlled by a UIViewController has the UIViewController as its nextResponder.
id object = theSubview;
do {
object = [object nextResponder];
} while ( ![object isMemberOfClass:[YourViewController class]] );
// object has the view controller you need.
I have been trying to add/implement this example to my existing Split View app tests.
Apple Example
I what to use the concept of replacing the detail view or right view, otherwise my app will be different. It is this difference that is causing my problems.
I have a rootviewcontroller or left view and upon choosing something here a new view is pushed onto this view. Upon choosing something in this "pushed view" I want to change the detail view or right hand view. This is the difference to apples example where the rootview does not have a pushed view on it and thus references are not broken.
Below is my change code - the new View DVCases is being initialized but the didload is not happening.
The issues are learner issues to do with my classes.
This below code is in my RootViewController implementation code but my reference to splitviewcontroller is not working if there is a new view pushed.
Second self.navigationcontroller is not correct because I have pushed a second view to the rootviewcontroller.
To centralize and simplify the code what I have done is from the delegate of the pushed view in the didselect event i call a method found in the rootviewcontroller passing the index as a parameter. The code for my custom method contains what is below.
So my question is how do I do this in my situation where I have pushed other views onto the rootview or left side. It appears that after pushing a view the reference to splitviewcontroller is gone and self.navigationcontroller is also gone/or wrong.
UIViewController <SubstitutableDetailViewController> *detailViewController = nil;
if (value == 0) {
DVCases *newDetailViewController = [[DVCases alloc] initWithNibName:#"DVCases" bundle:nil];
detailViewController = newDetailViewController;
}
// Update the split view controller's view controllers array.
NSArray *viewControllers = [[NSArray alloc] initWithObjects:self.navigationController, detailViewController, nil];
splitViewController.viewControllers = viewControllers;
[viewControllers release];
// Dismiss the popover if it's present.
if (popoverController != nil) {
[popoverController dismissPopoverAnimated:YES];
}
// Configure the new view controller's popover button (after the view has been displayed and its toolbar/navigation bar has been created).
if (rootPopoverButtonItem != nil) {
[detailViewController showRootPopoverButtonItem:self.rootPopoverButtonItem];
}
[detailViewController release];
I would appreciate any tips or help you might have.
Initialization of any viewcontroller class does not mean that it will make call to viewDidLoad method.
viewDidLoad method will only be called when you load view of that viewController. Generally we do it either by following methods.
1. Pushing it on navigation stack.
2. Presenting it using modal transition.
3. Adding it on some other view using [someView addSubView:controller.view];
4. Selecting any tabBar item for the first time Or tapping tabBar Item twice.
there may be some other scenarios.
But right now in your code I don't see any of this element.
Initialization means you are calling the direct method for intialization(calling its constructor) like here in above code initWithNibName will call this method of DVClass not any other(until this method had call for other methods inside it).
Thanks
As I am learning to properly code - my problems centres around that.
The above code is perfect as long as you call it using the same instance. I was not. Thus it was not working.
In the end I made my RootViewController a delegate for a method that has the above code. Thus when in another view - this view can call this method and the proper or real instance of RootViewController will implement it.