in my current application, I want to use a certain UICollectionView several times, but with different selection behaviours. Consider the following storyboard layout as "is" (and working):
Tab Bar Controller (2 items):
-> Navigation Controller 1 -> Collection View Controller -> some Table View Controller
-> Navigation Controller 2 -> (Basic) View Controller
The Basic View Controller has two UIButtons which have Storyboard Push-connections to the Collection View Controller. What I want is to transition from the Basic View Controller to the Collection View Controller, but selecting an item from the collection should pop the view and return to the Basic View Controller.
I have set a custom property in the Collection View Controller which gets set in the corresponding prepareForSegue message of the Basic View Controller (or not at all, if the user selects a Tab Bar Item), so there's no problem in detecting which controller or which UI component triggered the push (there are 3 ways: selecting the tab bar item or tapping one of the buttons on Basic View).
The problem is popping the Collection View.
My code so far:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ( self.mode == nil ) {
// do nothing
} else if ( [self.mode isEqualToString:#"foobar"] ) {
// one way I tried
[self dismissViewControllerAnimated:YES completion:nil];
} else if ( [self.mode isEqualToString:#"blah"] ) {
// other method
BasicViewController *targetVC = self.navigationController.viewControllers[ 0 ];
[self.navigationController popToViewController:targetVC animated:YES];
}
}
Unfortunately, my app crashes in the lines dismiss resp. popToViewController. Is it even possible to use the same view controllers in different ways of navigation?
I hope it's enough information to help me out on this one. As you might know, projects grow, and I don't know if there's more code to consider :)
Thanks in advance!
The prepareForSegue: method is not the right place to put that code. It's called right before the segue is performed, which usually means there's allready some kind of transition about to happen.
I'm assuming you've connected your collection view cells with a segue and are now trying to modify the behaviour of that segue depending on the viewcontroller 'before' the collectionVC.
In that case there are some possible solutions:
don't use segues for that specific transition and do any vc-transitions manually
write your own UIStoryboardSegue subclass that can handle the different transitions. See UIStoryboardSegue Class Reference. Then you can set some property of your custom segue in the prepareForSegue: method, to tell the segue which transition it should perform.
use unwinding segues. See What are Unwind segues for and how to use them?
The action of a selected a row in your 'Collection View Controller' differs depending on from where the 'Collection View Controller' was presented. In one case, return to 'Basic View Controller' and in the other segue to 'some Table View Controller'
Implement this by defining tableView:didSelectRowAtIndexPath: with something like:
- (void) tableView: (UITableView *) tableView
didSelectRowAtIndexPath: (NSIndexPath *) indexPath
{
if (self.parentIsBasicViewController) // set in the incoming segue, probably.
[self.presentingviewController dismissViewControllerAnimacted: YES completion: nil]
else
[self performSegueWithIdentifier: #"some Table View Controller" sender: self];
}
Related
i have an app with 5 views, mostly consisting of drill downs.
lets say i drill down to the 4th view controller. is there a way to present the second view controller exactly as it is without recreating it and modally present that view?
the drill downs do a sort of round about and i dont want to force the user to reselect their selection on the first view to bring them into the second view
so its like this (tvc = tableviewcontroller)
tvc1 > tvc2 > tvc3 > tvc4 > tvc2 > tvc5
^ ^
these two views are the same view in memory
You cannot present tvc2 again while it is already in the "stack" of presented view controllers.
If you push your view controllers onto a navigation controller's stack, then you can change the order of the view controllers in that stack by assigning to its viewControllers property or by sending it setViewControllers:animated:. You can hide its navigation bar if you don't want users to see it. I don't think it's safe to put the same view controller into the stack in two places at the same time.
You can possibly iterate over the visible viewControllers, and use the is casting operator to check if it is that type of class. Then find the view controller and pop to it.
Objective-C
for (id viewController in self.navigationController.viewControllers) {
if ([viewController isKindOfClass:[ViewControllerClass class]]) {
[self.navigationController popToViewController:viewController animated:TRUE];
}
}
Swift 2.0
for viewController in self.navigationController!.viewControllers {
if viewController is ViewControllerClass {
self.navigationController?.popToViewController(viewController, animated: true)
}
}
UINavigationController has an array called viewControllers. This will give you the list of UIViewControllers that exist in that navigation stack.
You can try something like:
UIViewController *yourTableViewController = [self.navigationController.viewControllers objectAtIndex:(theIndexOfYourTableViewController)];
Hope this helps.
Assume we have three view controllers: 1, 2, and 3. Using the storyboard, it's pretty simple to unwind from view controller 3 to view controller 1 using an unwind segue. However, when unwinding, view controller 2 is briefly visible before view controller 1 is displayed. Is there any way to get from 3 to 1 without displaying 2 again?
View Controller 1:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(#"one did appear");
}
- (IBAction)goToTwo:(id)sender {
NSLog(#"#### segue to two");
[self performSegueWithIdentifier:#"TwoSegue" sender:self];
}
- (IBAction)unwindToOne:(UIStoryboardSegue *)sender {
}
View Controller 2:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(#"two did appear");
}
- (IBAction)goToThree:(id)sender {
NSLog(#"#### segue to three");
[self performSegueWithIdentifier:#"ThreeSegue" sender:self];
}
View Controller 3:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
NSLog(#"three did appear");
}
- (IBAction)unwindToOne:(id)sender {
NSLog(#"#### unwind to one");
[self performSegueWithIdentifier:#"OneSegue" sender:self];
}
This produces the following log messages:
one did appear
segue to two
two did appear
segue to three
three did appear
unwind to one
two did appear
one did appear
I've tried using custom segues and disabling animation. Although removing animation makes view controller 2 appear for an even shorter period of time, it still appears. Is there any way to program this behavior?
Screenshot of storyboard:
This seems to be due to the way that unwind segues search for the nearest view controller which implements the unwind action you specified in the storyboard. From the documentation:
How an Unwind Segue Selects its Destination
When an unwind segue is triggered, it must locate the nearest view controller that implements the unwind action specified when the unwind segue was defined. This view controller becomes the destination of the unwind segue. If no suitable view controller is found, the unwind segue is aborted. The search order is as follows:
A viewControllerForUnwindSegueAction:fromViewController:withSender: message is sent to the parent of the source view controller.
A viewControllerForUnwindSegueAction:fromViewController:withSender: message is sent to the next parent view controller [...]
You can override canPerformUnwindSegueAction:fromViewController:withSender: if you have specific requirements for whether your view controller should handle an unwind action.
The view controllers in your example don't have a parent, so it seems that the fallback (which I can't see documentation for) is to instantiate each presentingViewController and call canPerformUnwindSegueAction on them in turn. Returning NO from this method in ViewControllerTwo doesn't prevent its instantiation and display, so that doesn't solve the issue.
I've tried your code embedded within a navigation controller, and it works fine. This is because in that case, UINavigationController is the parent of each of your view controllers, and it handles all the selection of a destination view controller. More documentation:
Container View Controllers
Container view controllers have two responsibilities in the unwind process, both discussed below. If you are using the container view controllers provided by the SDK, such as UINavigationController, these are handled automatically.
If you were to create a simple container view controller to act as the parent for your three view controllers, you could use its viewControllerForUnwindSegueAction method to check each of its child controllers for the existence of the unwind action, before calling canPerformUnwindSegueAction on that controller, and finally returning the first one of those which returns YES.
Selecting a Child View Controller to Handle An Unwind Action
As mentioned in How an Unwind Segue Selects its Destination, the source view controller of an unwind segue defers to its parent to locate a view controller that wants to handle the unwind action. Your container view controller should override the method shown in Listing 2 to search its child view controllers for a view controller that wants to handle the unwind action. If none of a container's child view controllers want to handle the unwind action, it should invoke the super's implementation and return the result.
A container view controller should search its children in a way that makes sense for that container. For example, UINavigationController searches backwards through its viewControllers array, starting from the view at the top of the navigation stack.
Listing 2 Override this method to return a view controller that wants to handle the unwind action.
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender
Container view controller design has a whole article dedicated to it by Apple, which I won't duplicate here (more than enough of Apple's writing in this answer already!) but it looks like it will take some thought to get it right, as it depends on the exact role you want these to play in your application.
A quick workaround, to get your desired behaviour using unwind segues, could be to embed the view controllers in a UINavigationController, and then hide the navigation bar using
[self.navigationController setNavigationBarHidden:YES];
Josh's answer led me to a solution. Here's how to accomplish this:
Create a root UINavigationController, and assign it to a class that extends UINavigationController and overrides the segueForUnwindingToViewController:fromViewController:identifier method. This could be filtered by the identifier if desired.
CustomNavigationController:
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController fromViewController:(UIViewController *)fromViewController identifier:(NSString *)identifier {
return [[CustomUnwindSegue alloc] initWithIdentifier:identifier source:fromViewController destination:toViewController];
}
Create a custom push segue, that behaves like a modal segue, but utilizes our navigation controller. Use this for all "push" segues.
CustomPushSegue:
-(void) perform{
[[self.sourceViewController navigationController] pushViewController:self.destinationViewController animated:NO];
}
Create a custom unwind segue, that uses the navigation controller to pop to the destination. This is called by our navigation controller in the segueForUnwindingToViewController:fromViewController:identifier method.
CustomUnwindSegue:
- (void)perform {
[[self.destinationViewController navigationController] popToViewController:self.destinationViewController animated:NO];
}
By utilizing a combination of these patterns, the second view controller never appears during the unwind process.
New log output:
one did appear
#### segue to two
two did appear
#### segue to three
three did appear
#### unwind to one
one did appear
I hope this helps someone else with the same issue.
Looks like custom segues are being used, so it's possible that's interfering somehow, although I've never seen it happn. I suggest you check out Apple's example project. It also has custom segues in it so it should serve as a good starting point for you.
Apple's Unwind Segue Example
I surprised you're seeing the behavior you're seeing, but one way to change it would be to use an explicit dismiss rather than unwind segues (this assumes the forward segues are modal).
Everything will look right if VC1 does this:
[self dismissViewControllerAnimated:YES completion:^{}];
Or if some other vc does this:
[vc1 dismissViewControllerAnimated:YES completion:^{}];
The only hitch is that you'll need a handle to vc1 if you want to dismiss from some other vc. You could use a delegate pattern to let vc1 know it should do the dismiss, but a simpler solution is to have vc2 or 3 post a notification when the unwind should happen.
VCs 2 or 3 can do this:
// whenever we want to dismiss
[[NSNotificationCenter defaultCenter] postNotificationName:#"TimeToDismiss" object:self];
And VC1 can do this:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(doDismiss:)
name:#"TimeToDismiss"
object:nil];
- (void)doDismiss:(NSNotification *)notification {
[self dismissViewControllerAnimated:YES completion:^{}];
}
While trying out storyboards for one of my projects I came across something for which I don't have a good solution;
I have a navigation-based application which shows a UITableViewController. The tableView is populated with user-created elements. Tapping an element cell brings up the detail view controller. The user can create a new element by tapping a button in the tableView. This brings up a modal view, which will handle the creation.
Now, when a user is done with creating the element and dismisses the modal view controller, I want the user to see the corresponding new detail view controller and not the tableview. But I can't figure out how to achieve this in storyboards.
Does anyone have a good pattern for this?
Current situation
TableView --(tap create)--> creation modal view --(finish creating)--> TableView
Should be
TableView --(tap create)--> creation modal view --(finish creating)--> detail view
You can put the creating view controller in a navigation controller and link the creation view controller to the detail view controller as well with a push segue. When you finish creating the data it will direct to an instance of detail view controller.
If you want to navigate back from details view directly to the table view, you can add a property to the details view controller, say #property (nonatomic) BOOL cameFromCreationViewController;. You can set this property in prepareForSegue: in the source view controller. In the details view make your own back button, and when it's tapped, you can do this:
if(self.cameFromCreationViewController){
[self.presentingViewController dismissViewController];
}
else {
[self.navigationController popViewController]
}
The best pattern I could come up with, is the same as the old pattern in code.
Add a property (nonatomic, weak) UINavigationController *sourceNavigationController to the modal view controller. When it becomes time to dismiss the modal view controller, add the following code:
DetailViewController *detailViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"DetailViewController"];
detailViewController.element = newlyCreatedElement;
[[self sourceNavigationController] pushViewController:detailViewController animated:NO];
And to make sure the sourceNavigationController get set properly, add the following code in the prepareForSegue: of the TableView:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
if ([segue.identifier isEqualToString:#"newElementSegue"]) {
[[segue destinationViewController] setSourceNavigationController:self.navigationController];
}
}
Basically what I want to do is to include UIView to multiple UIViewControllers on storyboard. I could include the uiview, but segues in the UIView doesn't work.
I have a storyboard something like this:
I have a tab controller with 2 UIViewControllers First and Second. And I have a separated UITableViewController with two another UIViewControllers A and B, connected with segues.
I could add the table view into First and Second views as a subview, but when I tap cell it doesn't go to next screen A or B. I sort of figured out why it didn't work, but just can't figure the best way to accomplish this.
Is there any good way to do this? I'm new to storyboard but have been developing iOS app for a while.
EDIT:
The way I add the tableViewController to each View is following:
self.tableViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"theTableViewController"];
[self.view addSubview:_tableViewController.tableView];
When a cell in the table view is tapped, prepareForSegue:segue:sender is invoked, but no push to navigation controller since the table view controller is sitting in each view controller as just a subview
EDIT2:
I posted my test project here
There are 2 methods you can try .
1.Create a segue between the tableview cell and next view controller directly. What I want to emphasize is the fact that do not create segue between the tableview or view and the next view controller,unless trying the method below.
2.Add this code to your tableview controller :
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([indexPath row]) {
[self performSegueWithIdentifier:#"segueToB" sender:self];
}else {
[self performSegueWithIdentifier:#"segueToA" sender:self];
}
}
I finally figured out how to make segue within sub views. It was actually very simple. It seems that I need to add the view controllers to the main view controller as a "childViewControllers"
instead of
self.tableViewController = [self.storyboard instantiateViewControllerWithIdentifier:#"theTableViewController"];
I did like this:
[self addChildViewController:[self.storyboard instantiateViewControllerWithIdentifier:#"theTableViewController"];
[self.view addSubview:_tableViewController.tableView];
and segues in the childViewController work as I expect. Thank you guys for your help.
My problem seems like a generic problem, yet can't seem to find an answer for it.
I have a situation where when the user taps on a custom UITableViewCell, I would like to display an alert and then based on the response to the alert, either stay on the same view (user selecting cancel) or display another view (if the user selects proceed). And I would like to do this using the storyboard feature & segues.
How would one go about this? Do you have to do this the old fashioned way?
#user, Just create the alertView the old fashion way; I do know of any storyboard feature to do this differently. Where storyboard can help is with the segues. You can call the segues programmatically. With you alert view cancel button you can just return (i.e. do nothing). For the other option, to display another view, you can programmatically call a segue to transition to the desired view. If you don't have the proper segue already defined for some other reason on your storyboard, just create a button out and use that to create the segue and name it. Name the segue by clicking on it in storyboard and use the attributes inspector to give it name (identifier). Then hide the button or put it out of the view. I typically put these type of button on the toolbar and use spacers to keep them out of the view. Here's some sample code:
Call the segue from the alert view delegate like this:
[self performSegueWithIdentifier: #"done" sender: self];
Also implement this method to do any necessary task to prepare for the segue:
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if ([[segue identifier] isEqualToString:#"done"])
{
// [[segue destinationViewController] setManagedObjectContext:self.managedObjectContext];
// [[segue destinationViewController] setSelectedClient:selectedClient];
}
}
You can create segues directly from the startingViewController to multiple destinationViewControllers that can then be "performed" programmatically. You do not need to create any hidden buttons for them, which does seem like a hack.
OK I came up with a solution in keeping with the storyboard that I like.
Example:
My tableview has 2 sections, grouped, and cells are dynamic prototype. Section 0 contains one row/UITableViewCell & I don't want it to segue. Section 1 contains multiple cells that I want to trigger the segue & drill down into the detail.
In Storyboard:
I removed the segue linking the tableviewcell to the destination view controller.
I made a 'generic' segue linking the source view controller directly to the destination view controller.
In the attributes on the segue, I set the identifier ('EditTimePeriod') and set the type to Push (I presume Modal would work just the same).
In the source view controller:
In the prepareForSegue method I handled both the common 'AddTimePeriod' segue I control-dragged from my UIBarButtonItem (Add), along with the 'generic'(vc-->vc) 'EditTimePeriod' segue.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// handle the click of the 'Add' bar button item
if([segue.identifier isEqualToString:#"AddTimePeriod"]) {
TimePeriodViewController* tpvc = (TimePeriodViewController*)segue.destinationViewController;
tpvc.delegate = self;
// database & entity stuff for adding the new one to the mOC, etc
}
// handle the click of one of the 'editable' cells -
if([segue.identifier isEqualToString:#"EditTimePeriod"]) {
TimePeriodViewController* tpvc = (TimePeriodViewController*)segue.destinationViewController;
tpvc.delegate = self;
TimePeriod * newTP = [self.timePeriodArray objectAtIndex:self.tableView.indexPathForSelectedRow.row];
tpvc.timePeriod = newTP;
}
}
Then I implemented the tableView:didSelectRowAtIndexPath method, and put my condition in here. If the selected row was outside of section zero I called the EditTimePeriod segue manually, defining the sender as the selected tableviewcell:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if(self.tableView.indexPathForSelectedRow.section!=0){
[self performSegueWithIdentifier:#"EditTimePeriod" sender:[tableView cellForRowAtIndexPath:indexPath]];
}
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
return;
}
would be nice to code the cell in section 0 so that it is not selectable in the first place!
Hope this helps though.
** and then 5 minutes later I took another look and realized I could just move the data from section 0 into the section header, which is more intuitive and wasn't being used anyway. leaving the design open for a standard segue from each tableviewcell without needing any condition/check. Was a good exercise anyway though :)