I have one button (back button) on a view controller. Simple so far.
I have 2 view controllers with a table on each one.
if a user selects a row from either table it goes to the view controller with the back button on.
The back button needs to go back to the original view controller that the user was on when they selected the row.
I was considering unwind segues for this which is fine, but I cannot add two segues to one button. One for return to one table view and a return for the other table view dependent on which table view they used to access the view controller.
Any ideas ?
As Volk explained,
-(IBAction)devDismiss
{
NSLog(#" ------- dev dismiss .....");
// for a custom segue unwind, you must do this...
[self performSegueWithIdentifier:#"specialWord" sender:self];
// "specialWord" is the identifier set on storyboard for the
// custom unwind segue
/* for a "default" unwind segue, do this...
[self dismissViewControllerAnimated:YES completion:nil];
Note, if you used a push segue, you should use
[self.navigationController popViewControllerAnimated:YES]
If you used a modal segue, you should use
[self dismissViewControllerAnimated:YES completion:nil] "
*/
}
Note that indeed, you must also use "specialWord" in your segueForUnwindingToViewController: override, which will be in the DESTINATION (that is to say the ORIGINAL) view controller, the one underneath, that is to say.
-(UIStoryboardSegue *)segueForUnwindingToViewController:
(UIViewController *)toViewController
fromViewController:(UIViewController *)fromViewController
identifier:(NSString *)identifier
{
NSLog(#"we're in _your class name_, segueForUnwindingToViewController %#",
identifier);
// for some unwinds, we have a custom unwind we want to use.
// so, check the identifier:
if ([identifier isEqualToString:#"specialWord"])
{
YourCustomUnwindSegue *sg = [[YourCustomUnwindSegue alloc]
initWithIdentifier:identifier
source:fromViewController
destination:toViewController];
return sg;
}
// don't forget the break-away "return" inside any macthes.
// NSLog(#"note, if this message appears, it's likely you have a typo
// somewhere for 'specialWord' - unless you genuinely have a situation
// where it will also fall through to the 'default' unwind segue :O ");
// BE SURE TO return the default unwind segue otherwise
return [super segueForUnwindingToViewController:toViewController
fromViewController:fromViewController
identifier:identifier];
}
Related
I want to perform some code when the segue goes back one step. For example, when the back button gets selected, I want to perform some code.
I can't create a new unwind action from the back button because there is no back button in the storyboard. Is there a way to insert code, as soon as the back button is selected?
If you want to stick with the default back button, one way to do this is to subclass the navigation controller, and override popViewControllerAnimated: which is called when you tap the back button.
-(UIViewController *)popViewControllerAnimated:(BOOL)animated {
UIViewController *vcToBePopped =[super popViewControllerAnimated:animated];
id vc = self.viewControllers.lastObject; // this will be the controller you're going back to
if ([vc isKindOfClass:IntendedViewController class]) { // replace IntendedViewController with the actual class name you care about
[(IntendedViewController *)vc someMethod];
}
return vcToBePopped;
}
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:^{}];
}
I have the following storyboard in an application I am working on:
At the root, I have a Tab Bar Controller. It links to two View Controllers.
The first View Controller to display a newsfeed with pictures uploaded by the user (the one at the bottom in the storyboard).
The second View Controller serves to initiate the taking of a picture and attach some data to it. In the last step (top right), when touching "Save" in the right item of the Navigation bar, I want the user to be redirected to the newsfeed View Controller passing it some data.
I tried using a segue and it works. The data are passed to the newsfeed but the wrong tab is selected. I changed the selected tab using
[self.tabBarController setSelectedIndex:0];
But by tapping on the second tab again, things are messed up. I can see the newsfeed instead of the taking a picture screen. If I tap again, it crashes.
At some point I thought I may have got the wrong storyboard and should have implemented a TabBar in my newsfeed and handle the taking picture as a modal view.
Would you know any clean way to achieve this?
Thanks
You should not use a normal segue, which adds the destination controller to the stack. To do what you are trying to the best way should be to use an unwind segue. This is a rough sketch of what you need to do:
• Declare an unwind segue action in the NewsfeedController like (IBAction)unwindFromPictureSaved:(UIStoryboardSegue *)segue;
• Connect your "Save" button in your SavingPictureController to the "Exit" icon in the storyboard and select the previously defined method;
• In the newly created unwind segue define its identifier with something like SavedPictureSegue;
• Define the data to be passed in SavingPictureController's header with something like #property (strong, readonly, nonatomic) id passedData;
• In SavingPictureController implement
-(void)prepareForSegue:(UIStoryboardSegue *)segue
{
if ([segue.identifier isEqualToString:#"SavedPictureSegue"]) {
_passedData = // Your data here
}
}
• In NewsfeedController now implement the previously defined method and fetch the data from (SavingPictureController *)segue.sourceController. Be sure to #import "SavingPictureController.h".
Thanks to #Davide, I created a subclass of TabBarController and implemented the method below:
// Find the appropriate controller to answer to an unwind segue
// For each child view controller
// Checks if it is a Navigation Controller
// If it is check its children view controllers
// Return the first view controller that answers the unwind segue
// This because I assumed the default behavior is just to check one level up (in this case, it would have stopped at the NavigationController)
// Based on https://developer.apple.com/library/ios/technotes/tn2298/_index.html#//apple_ref/doc/uid/DTS40013591-CH1-CCVC-SELECTING_A_CHILD_VIEW_CONTROLLER_TO_HANDLE_AN_UNWIND_ACTION
- (UIViewController *)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender {
BOOL resChildren, res;
for(UIViewController *controller in self.childViewControllers) {
if ([controller isKindOfClass:[UINavigationController class]]) {
for (UIViewController *childController in controller.childViewControllers) {
resChildren = [childController canPerformUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
if (resChildren) {
return childController;
}
}
}
res = [controller canPerformUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
if (res) {
return controller;
}
}
return nil;
}
Then in the unwind method of the 'NewsFeedController" it is necessary to set the correct index to see the controller with something like:
[self.tabBarController setSelectedIndex:1];
I uploaded a demo on github at https://github.com/kintso/unwindSegueWithTabBarControllerAndNavigationController
I have one original view controller with four destination view controllers. I want to be able to push segue with a navigation controller to ALL of the destination view controllers from the original. I have tried...
- (IBAction)notificationsButtonPushed:(id)sender {
NotificationsViewController *notifications = [[NotificationsViewController alloc]init];
[self.navigationController pushViewController:notifications animated:YES];
}
- (IBAction)messagesButtonPushed:(id)sender {
MessagesViewController *messages = [[MessagesViewController alloc] init];
[self.navigationController pushViewController:messages animated:YES];
}
- (IBAction)settingsButtonPushed:(id)sender {
if (canMessage) {
SettingsViewController *settings = [[SettingsViewController alloc]init];
[self.navigationController pushViewController:settings animated:YES];
}
else {
NSLog(#"Can't Message");
}
}
- (IBAction)previewButtonPushed:(id)sender {
PreviewViewController *preview = [[PreviewViewController alloc]init];
[self.navigationController pushViewController:preview animated:YES];
}
and this just gives me an empty view controller without my UI components.
Note: I also have tired "initWithNidName:" and passed in the storyboardID of each destination view controller and it gives me Error:
'Could not load NIB in bundle: 'NSBundle </var/mobile/Applications/B7E025E5-D7D2-4FFD-B49C-E10DF5E94C44/LifePoints.app> (loaded)' with name 'preview'
I also have tried... (with storyboard segues set to "Push")
- (IBAction)notificationsButtonPushed:(id)sender {
[self performSegueWithIdentifier:#"notifications" sender:self];
}
- (IBAction)messagesButtonPushed:(id)sender {
if (canMessage) {
[self performSegueWithIdentifier:#"messages" sender:self];
}
else {
NSLog(#"Can't Message");
}
}
- (IBAction)settingsButtonPushed:(id)sender {
[self performSegueWithIdentifier:#"settings" sender:self];
}
- (IBAction)previewButtonPushed:(id)sender {
[self performSegueWithIdentifier:#"preview" sender:self];
}
Although this does push the destination view controllers onto the screen with the appropriate segue type, it does not segue to the correct destination view controller. It only seems to segue to the last attached storyboard segue.
Does anyone know how to correctly apply this and have it behave in the form I am looking for?
EDIT
It is important to note I am checking to see if a condition is met the "messagesButtonPushed" method. I am checking to see if user is allowed to message, and if they are, then segue to the VC.
You don't need any code to implement the basic segues.
In your story board ensure that your original view controller is embedded in a navigation controller (Select the original View and select Edit->Embed in->Navigation Controller).
Then you can simply control drag from each of your four buttons to the corresponding destination views, selecting "push" as the segue type. You can then click on the segue icon between the two views to give each segue an identifier.
You can delete your IBAction methods and any actions on the buttons that link to them.
I suggest you complete a Storyboard Tutorial to learn how storyboards and segues work.
If you do want to perform a segue programatically then you can control drag from the yellow view controller icon at the bottom of the view to the destination. You can then invoke the segue with performSegueWithIdentifier as per your second code - In your case your could have two different segues and trigger the store segue or the other one depending on purchase status
I have a situation where two view controllers are loaded on top of the initial view controller, and when the third view controller is loaded I would like to dismiss the two view controllers for the action of a button on the third view controller. Right now the button is only dismissing one view controller with the following code,
- (IBAction)logout:(id)sender {
[serial close];
if([self.view isKindOfClass:[ViewControllerCreate class]] ) {
[self dismissModalViewControllerAnimated:YES];
}
[self dismissModalViewControllerAnimated:YES];
}
I don't need to dismiss two view controllers every time, just when this particular situation presents itself.
For clarity sake, there is a button in the first view controller which presents the second view controller when pressed, then when the second view controller is loaded, there is a button when pressed presents the third view controller.
You should consider using dismissViewControllerAnimated:completion: which would allow you to chain multiple dismisses. Dismiss the first, pass in a completion to check for the necessary requirements to dismiss the second, etc.
I now have the desired behavior using the following code,
- (IBAction)logout:(id)sender {
[serial close];
if([self.presentingViewController isKindOfClass:[ViewControllerCreate class]] ) {
[self.presentingViewController.presentingViewController dismissModalViewControllerAnimated:YES];
}
[self dismissModalViewControllerAnimated:YES];
}