Unwind from custom modal transition - objective-c

I use custom transitions for modal segue, implemented using UIViewControllerAnimatedTransitioning delegate. All segues setup via Storyboard.
I have two nav controllers, both have single root VC setup. Let's call them 1st NVC, 1st VC, 2nd NVC and 2nd VC.
1st VC adopts the UIViewControllerAnimatedTransitioning protocol, implementation simply returns animator:
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
return [ModalTransitionAnimator new];
}
- (id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
return [ModalTransitionAnimator new];
}
When any segue happens from 1st VC -> 2nd NVC, 1st VC catches the event in prepareForSegue: and assigns transitioningDelegate along with modalPresentationStyle because you cannot do that from IB:
UINavigationController* destination = (UINavigationController*)segue.destinationViewController;
destination.transitioningDelegate = self;
destination.modalPresentationStyle = UIModalPresentationCustom;
So everything seems fine, I get a very cool animation running, destination controller shows up, all viewWillAppear: and viewDidAppear: fire in the right order.
And so now on screen, I have 2nd NVC with 2nd VC inside.
I tap "Cancel" button in Navigation bar, unwind action triggers on 1st VC, but no backward animation happening. I mean in fact 2nd VC is still on screen.
So apparently nobody calls dismissViewControllerAnimated. I suppose Storyboard is confused and can't find the way back because 2nd NVC was initially modally presented and then some other controller requests unwinding. How do you deal with that normally?

I followed the unwind chain and found that UIKit was not able to find the right unwind segue after state restoration. Before state restoration everything worked perfectly fine.
I fixed it manually and traversed my controllers hierarchy to return a controller that has unwind action defined:
- (UIViewController*)viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender {
if([NSStringFromSelector(action) hasPrefix:#"unwindToInitialViewController"]) {
return [[((UINavigationController*)self.centerViewController) viewControllers] firstObject];
}
UIViewController* c = [super viewControllerForUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
DDLogInfo(#"%s = %#, c = %#", __PRETTY_FUNCTION__, NSStringFromSelector(action), c);
return c;
}

Related

Storyboard - Popping to a View Controller then Pushing Causes multiple pushes

In Objective C / iOS;
We have a process similar to this (set up in xcode storyboard);
Menu View Controller -> Enter in a code -> Process/Validate -> Present a Failed Code page
The (->) arrows signify a push segue setup in storyboard
When in failed state I want to pop to the Enter in a code view controller.
UIViewController *vc = nil;
NSUInteger index=0;
for (UIViewController *viewController in self.navigationController.viewControllers) {
if ([viewController isKindOfClass:[SomeViewController class]]) {
vc = viewController;
break;
}
index++;
}
if (vc) {
dispatch_async(dispatch_get_main_queue(), ^(void) {
[self.navigationController popToViewController:vc animated:YES];
});
return;
}
This pops me back to the VC I want to go to.
Except now when I press a submit on the Enter code page it does 3 or 4 more "pushes", when it should only be 1.
Do I need to unwind the segue? I tried emptying the navigational view controller stack, and I even tried ridding it of its last active view controller -- both of these return a blank or black window view frame.
Why would popping a view controller in the navigation stack affect the segues in my view controller to the point where whenever I try to do a push segue action it will try to push multiple view controllers onto the stack?
Turns out I had the following issue
Button press causes segue action in storyboard
I did the same segue action in code on the button, hence pushing multiple times
I have now solved this issue

UIPageViewController within NavigationController

I've read every tutorial I've found about UIPageViewController, but they show just basics, I'd like to create something like new twitter app has:
UIPageViewController is embedded into Navigation controller, title of navigation bar is based on current page and those page dots are there as well, user can tap on item on current page(item from table view/collection view) to see detail.
I was able to come up with something similar, each page had collection view, and showing detail of some item was reflected in navigation bar, there was correct title and "<" button, but I wasn't able to change title based on currently shown page
Please, could you describe me how to do this in few steps/basic structure of controllers?
Don't know if you are still working on this but here goes anyway. To set up a UIPageViewController you might use the tutorial and two questions below.
http://www.appcoda.com/uipageviewcontroller-storyboard-tutorial/
How to implement UIPageViewController that utilizes multiple ViewControllers
How to add UIBarButtonItem to NavigationBar while using UIPageViewController
The last link pertains specifically to setting the contents of the navigationBar depending on what you are viewing.
The key is to create a UINavigationItem Property in the .h file of your UIPageViewController content view controllers, meaning the ones/one that are displaying whatever it is you are displaying.
From my code in FirstViewController.h SecondViewController.h and ThirdViewController.h
#property (strong, nonatomic) UINavigationItem *navItem;
In the second and third links above you'll see a storyboard layout of a Master-Detail application (which uses a navigation controller). The UIPageViewControllerDataSource is the DetailViewController. The three pages associated with the pageViewController are my content view controllers.
In DetailViewController.m you have to instantiate the contentViewControllers somewhere. At that point you pass the DetailViewControllers navigationItem id to the content view controllers. Here is how I instantiate my content view controllers using the delegate methods of the UIPageViewController.
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
NSString * ident = viewController.restorationIdentifier;
NSUInteger index = [_vc indexOfObject:ident];
if ((index == 0) || (index == NSNotFound)) {
return nil;
}
index--;
if (index == 0) {
return [self controllerAtIndex:index];
}else if (index == 1){
return [self secondControllerAtIndex:index];
}else if (index == 2){
return [self thirdControllerAtIndex:index];
}else{
return nil;
}
}
The delegate method calls the method below. It is almost directly from the tutorial link with just a few modifications.
-(FirstController *)controllerAtIndex:(NSUInteger)index
{
FirstController *fvc = [self.storyboard instantiateViewControllerWithIdentifier:#"FirstPageController"];
fvc.imageFile = self.pageImages[index];
fvc.titleText = self.pageTitles[index];
fvc.pageIndex = index;
fvc.navItem = self.navigationItem;
return fvc;
}
Notice that properties are passed into the view controller including self.navigationItem. Passing it in ensures you can make changes to the navigationBar items.
Then in the viewDidAppear method of your content view controller you can simply set the title on the navigation bar like this.
navItem.navigationItem.title = #"Whatever you want the title to be";
It is important to use viewDidAppear because viewDidLoad is not called every time the screen appears. I believe the UIPageViewController caches the page before and the page after whatever you are viewing which saves the system from having to load the page every time you navigate to it.
If you are using a single view controller for all you pages like the tutorial does you will have to use the index property to know what to set the title to.
Hope this helps!
I had a very similar setup and solved the problem.
My setup is that I have a UIPageViewController inside a UINavigationController because I wanted the navigation bar to be persistent while I swiped between each view controller. I wanted the title of the current UIViewController inside the UIPageViewController to become the title of the UINavigationController.
The way to do this is to implement the UIPageViewControllerDelegate method pageViewController didFinishAnimating which triggers after a change to a view controller is made from the UIPageViewController. You can probably see where this going: From here, set the navigationItem.title property of the UIViewPageController, which the UINavigationController uses to set it's own title, with that of the current view controller's title.
Example:
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
{
if(finished && completed)
{
NSString *titleOfIncomingViewController = [[pageViewController.viewControllers firstObject] title];
pageViewController.navigationItem.title = titleOfIncomingViewController;
}
}
NB: This delegate method triggers only off gesture-initiated scrolls.
Solution given by Mr. Micnguyen is the exact solution but the mentioned delegate method didFinishAnimating() is called when swipe action is done due to which initially title is not shown.
Hence to resolve that problem, we need to set its initial value in viewDidLoad() method of UIPageViewController class as mentioned below:
- (void)viewDidLoad(){
self.navigationitem.title = arrayname[0];
}
I had this question too, but ended up doing something different, because I'm using Swift, so I thought I'd share here.
I ended up embedding a UIPageViewController in a Container View in my Navigation Controller. On a page swipe, I used pageViewController(_:didFinishAnimating:previousViewControllers:transitionCompleted:), with my PageViewController as the UIPageViewDelegate. From there I created a protocol that I used to send data about the VC displayed to the parent VC, and change the title using self.title = "My Title".
I didn't make the parent VC the UIPageViewDelegate because I found it to be easier to access the displayed VC from the PageViewController than from the parent VC as let childVC = pageViewController.viewControllers![0] as! DetailViewController.

how to display various view controllers (each having its respective navigation controller) using storyboard ID

I'm working on my first app. Here's what I want to accomplish:
There will be a menu with several different options. For simplicity, assume this is comprised of UIButtons with IBAction outlets and the functionality exists to pull up the menu at any time.
Each menu button, when pressed, should display a different navigation controller's content. If the user brings up the menu and makes a different selection, the navigation controller in which he is currently operating should not be affected; the newly selected navigation chain is displayed on top of the old, and through the menu, the user can go back to the view where he left off on the previous navigation chain at any time.
visual illustration (click for higher resolution):
Please note that there are 3 different navigation controllers/chains. The root view controller (which is also the menu in this simplified version) is not part of any of them. It will not suffice to instantiate one of the navigation chains anew when it has been previously instantiated, and here's why: if the user was on screen 3 of option 2 and then selects option 1 from the menu and then selects option 2 (again) from the menu, he should be looking at screen 3 of option 2--right where he left off; the view controller he was viewing when he previously left the navigation chain should be brought back to the top.
I can make a button instantiate and present a view controller from the storyboard if there is NOT a navigation controller:
- (IBAction)buttonPressed:(id)sender {
UIViewController *controller = [self.storyboard instantiateViewControllerWithIdentifier:#"View 2"];
[self presentViewController:controller animated:YES completion:nil];
}
However, I can't figure out how to make those two methods work with a navigation controller involved. Moreover, I'm not sure those two methods are the right choice, because I won't always want to instantiate a new view controller: when a menu button is pressed, a check should be performed to see if the view (navigation?) controller with the corresponding identifier has already been instantiated. If so, it should simply be made the top view controller.
In summary, here are my questions:
1) How should I instantiate and display a view controller that is embedded in a navigation controller, preferably using a storyboard ID? Do you use the storyboard ID of the navigation controller or of the view controller?
2) How should I check whether an instance already exists? Again, should I check for an extant navigation controller or for a view controller, and what's the best method to do so?
3) If the selected navigation chain has already been instantiated and is in the stack of view controllers somewhere, what is the best method for bringing it to the top?
Thank you!!
side note -- it would be nice to know how to paste code snippets with indentation and color formatting preserved :)
As Rob has suggested, a tab bar controller would make a good organising principle for your design.
Add a UITabBarController to your storyboard, give it a storyboard iD. Assign each of your three sets of viewControllers ( with their respective navController) to a tab item in the tabBarController.
UITabBarController
|--> UINavigationController --> VC1 ---> VC2 -->
|--> UINavigationController --> VC1 ---> VC2 -->
|--> UINavigationController --> VC1 ---> VC2 -->
In you app delegate make a strong property to hold your tab bar controller's pointer. As the tab bar controller keeps pointers to all of it's tab items, this will take care of state for each of your sets of viewControllers. You won't have to keep separate pointers for any of them, and you can get references to them via the tabBarController's viewControllers property.
#property (strong, nonatomic) UITabBarController* tabVC;
Initialise it on startup
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
UIStoryboard storyBoard =
[UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:nil];
self.tabVC = [storyBoard instantiateViewControllerWithIdentifier:#"tabVC"];
//hide the tab bar
for (UINavigationController* navController in self.tabVC.viewControllers)
[navController.viewControllers[0] setHidesBottomBarWhenPushed:YES];
return YES;
}
An alternative way to hide the tab bar is to check the "Hides bottom bar on push" box in the Attributes Inspector for each of the (initial) viewControllers. You don't have to do this for subsequent viewControllers, just the first one that will be seen in that tab item.
Then when you need to navigate to one of your navController groups…
- (IBAction)openTab:(UIButton*)sender {
AppDelegate* appDelegate =
(AppDelegate*)[[UIApplication sharedApplication] delegate];
if ([sender.titleLabel.text isEqualToString: #"Option 1"]) {
appDelegate.tabVC.selectedIndex = 0;
}else if ([sender.titleLabel.text isEqualToString: #"Option 2"]){
appDelegate.tabVC.selectedIndex = 1;
}else if ([sender.titleLabel.text isEqualToString: #"Option 3"]){
appDelegate.tabVC.selectedIndex = 2;
}
[self presentViewController:appDelegate.tabVC
animated:YES completion:nil];
}
(this example uses presentViewController, your app design may navigate this in other ways…)
update
If you want to do this without a tab bar controller, you can instantiate an array holding pointers to each of your nav controllers instead:
UINavigationController* ncA =
[storyboard instantiateViewControllerWithIdentifier:#"NCA"];
UINavigationController* ncB =
[storyboard instantiateViewControllerWithIdentifier:#"NCB"];
UINavigationController* ncC =
[storyboard instantiateViewControllerWithIdentifier:#"NCC"];
self.ncArray = #[ncA,ncB,ncC];
Which has the benefit of not having a tab bar to hide…
Then your selection looks like…
- (IBAction)openNav:(UIButton*)sender {
AppDelegate* appDelegate =
(AppDelegate*)[[UIApplication sharedApplication] delegate];
int idx = 0;
if ([sender.titleLabel.text isEqualToString: #"option 1"]) {
idx = 0;
}else if ([sender.titleLabel.text isEqualToString: #"option 2"]){
idx = 1;
}else if ([sender.titleLabel.text isEqualToString: #"option 3"]){
idx = 2;
}
[self presentViewController:appDelegate.ncArray[idx]
animated:YES completion:nil];
}
1 / You can instantiate a viewController in your viewDidLoad method of your main viewController, so it will be instantiate 1 time only.
Now if you want display your controller, you would better push it :
- (IBAction)buttonPressed:(id)sender {
// Declare your controller in your .h file and do :
controller = [self.storyboard instantiateViewControllerWithIdentifier:#"View 2"];
// Note you can move this line in the viewDidLoad method to be called only 1 time
// Then do not use :
// [self presentViewController:controller animated:YES completion:nil];
// Better to use :
[self.navigationController pushViewController:controller animated:YES];
}
2 / I'm not sure, but if you want to check if an instance already exist just check :
if (controller) {
// Some stuff here
} // I think this checks if controller is initiated.
3 / I know it's not a good advice but I would tell you to not worry about checking if your controller already exist, because I think it's easier to access your viewController by using the 2 lines again :
controller = [self.storyboard instantiateViewControllerWithIdentifier:#"View 2"];
[self.navigationController pushViewController:controller animated:YES];
4 / I'm not sure if colors can be used here because of a specific style sheets.
I'm not sure to really have the good answer to your question but I hope this will help you.

Unable to create unwind segues when using custom view controller containment

I'm attempting to convert our application to storyboards and have hit what I believe is a bug in the handling of unwind segues when dealing with custom container controllers. We have a view controller which displays another and uses the view controller containment api to do this, I wire up the segue in IB then select a custom class for the implementation. The perform method looks something like this:
-(void) perform {
UIViewController *container = [self sourceViewController];
UIViewController *child = [self destinationViewController];
[container addChildViewController:child];
[container.view addSubview:child.view];
child.view.center = container.view.center;
[UIView transitionWithView:container.view
duration:0.35
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
child.view.alpha = 1;
} completion:^(BOOL finished) {
[child didMoveToParentViewController:container];
}];
}
That works perfectly, however I can't make it perform the unwind segue back to the container controller. I override viewControllerForUnwindSegueAction: fromViewController: withSender: and ensure that it's returning the correct value:
-(UIViewController *) viewControllerForUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender {
id default = [super viewControllerForUnwindSegueAction:action fromViewController:fromViewController withSender:sender];
NSAssert1(default == self, #"Expected the default view controller to be self but was %#", default);
return default;
}
I can also confirm that canPerformUnwindSegueAction:fromViewController:withSender is being called and doing the right thing, but to be sure I overrode it to return YES
-(BOOL) canPerformUnwindSegueAction:(SEL)action fromViewController:(UIViewController *)fromViewController withSender:(id)sender {
return YES;
}
The next step I would expect to happen is for segueForUnwindingToViewController:fromViewController:identifier: to be called, however it never is. Instead the application crashes with an NSInternalInconsistencyException.
2012-10-01 10:56:33.627 UnwindSegues[12770:c07] *** Assertion failure in -[UIStoryboardUnwindSegueTemplate _perform:], /SourceCache/UIKit_Sim/UIKit-2372/UIStoryboardUnwindSegueTemplate.m:78
2012-10-01 10:56:33.628 UnwindSegues[12770:c07] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Could not find a view controller to execute unwinding for <USCustomContainerViewController: 0x75949a0>'
*** First throw call stack:
(0x1c8e012 0x10cbe7e 0x1c8de78 0xb61f35 0x581711 0x45ab54 0x10df705 0x16920 0x168b8 0xd7671 0xd7bcf 0xd6d38 0x4633f 0x46552 0x243aa 0x15cf8 0x1be9df9 0x1be9ad0 0x1c03bf5 0x1c03962 0x1c34bb6 0x1c33f44 0x1c33e1b 0x1be87e3 0x1be8668 0x1365c 0x1e7d 0x1da5)
libc++abi.dylib: terminate called throwing an exception
Has anyone successfully used unwind segues combined with the view controller containment APIs? Any idea what step I'm missing? I've uploaded a demo project to github which shows the issue in the simplest demonstration project I could come up with.
The problem in your example is that there's no there there. It's too simple. First, you create your container view controller in a rather odd way (you don't use the new IB "container view" which is there to help you do this). Second, you've got nothing to unwind: nothing was pushed or presented on top of anything.
I have a working example showing that canPerformUnwindSegueAction really is consulted up the parent chain, and that viewControllerForUnwindSegueAction and segueForUnwindingToViewController are called and effective, if present in the right place. See:
https://github.com/mattneub/Programming-iOS-Book-Examples/tree/master/ch19p640presentedViewControllerStoryboard2
I have now also created a fork of your original example on github, correcting it so that it illustrates these features:
https://github.com/mattneub/UnwindSegues
It isn't really a situation where "unwind" is needed, but it does show how "unwind" can be used when a custom container view controller is involved.
This seems to be a bug – I would also expect unwind segues to work as you implemented.
The workaround that I used is explicitly dismissing the presented view controller in the IBAction method:
- (UIStoryboardSegue *)segueForUnwindingToViewController:(UIViewController *)toViewController
fromViewController:(UIViewController *)fromViewController
identifier:(NSString *)identifier
{
return [[UIStoryboardSegue alloc] initWithIdentifier:identifier
source:fromViewController
destination:toViewController];
}
- (IBAction)unwind:(UIStoryboardSegue*)segue
{
UIViewController *vc = segue.sourceViewController;
[vc willMoveToParentViewController:nil];
if ([vc respondsToSelector:#selector(beginAppearanceTransition:animated:)]) {
[vc beginAppearanceTransition:NO animated:YES]; // iOS 6
}
UIView *modal = vc.view;
UIView *target = [[segue destinationViewController] view];
[UIView animateWithDuration:duration animations:^{
modal.frame = CGRectMake(0, target.bounds.size.height, modal.frame.size.width, modal.frame.size.height);
} completion:^(BOOL finished) {
[modal removeFromSuperview];
[vc removeFromParentViewController];
if ([vc respondsToSelector:#selector(endAppearanceTransition)]) {
[vc endAppearanceTransition];
}
}];
}
Brief history before the answer: I just ran into the same exact error message when trying to use multiple Container Views on one iPad screen in iOS 6 and calling unwind segues from code. At first I thought this may be a problem because my segue was created using Storyboards by CTRL-dragging from File Owner to Exit instead of from some UI control to Exit, but I got same results when I put test Close buttons on each VC and had them trigger the unwind segues. I realized that I'm trying to unwind an embed segue, not a modal/push/popup segue, so it makes sense that it fails to do it. After all, if the unwind segue succeeds and the view controller is unloaded from a Container View, iOS 6 thinks there'll just be an empty space on the screen in that spot. (In my case, I have another container view taking up screen real estate that's shown behind the container view which I'm trying to unload, but iOS doesn't know that since the two aren't connected in any way.)
Answer: this led me to realize that you can only unwind modal, push, or popover segues, be it within the main window or as part of a Navigation/Tab Controller. This is b/c iOS then knows that there was a previous VC responsible for the whole screen and it's safe to go back to it. So, in your case, I'd look into a way to tell iOS that your child container view is connected to your parent container view in a way that makes it safe to dismiss the child container view. For example, perform a modal/push/popover segue when displaying the child container view, or wrap both into a custom UINavigationController class (I assume you don't want the navigation bar, that's why custom class).
Sorry I can't give exact code, but this is the best I got to so far and I hope it's helpful.
Looks like this bug is fixed in iOS9.

prepareForSegue is not called after performSegue:withIdentifier: with popover style

I have a universal app, where I am sharing the same controller for a IPad and IPhone storyboard.
I have put a UILongPressGestureRecognizer on a UITableView, that when a cell is pressed on iPhone it calls an action that perform a segue:
-(IBAction)showDetail:(id)sender {
UILongPressGestureRecognizer *gesture = (UILongPressGestureRecognizer*)sender;
if (gesture.state == UIGestureRecognizerStateBegan) {
CGPoint p = [gesture locationInView:self.theTableView];
NSIndexPath *indexPath = [self.theTableView indexPathForRowAtPoint:p];
if (indexPath != nil) {
[self performSegueWithIdentifier:SEGUE_DETAIL sender:indexPath];
}
}
}
the segue is a detail view performed as a 'push'. The first thing you should notice is that the sender is an NSIndexPath, is the only way I found for passing the selected cell. Maybe there's a better solution.
Everything works fine, in a sense that the segue is performed, and before the prepareForSegue is called too.
However it happens that on iPad, I have changed the segue identifier to Popover.
Now things are working in part, the segue is performed, but prepareForSegue is not called and therefore the destination view controller is not set up as it should be.
What am I doing wrong ?
What I have discovered so far, is that with any segue identifier that is not popover these are the invocations made by iOS:
prepareForSegue (on source controller)
viewDidLoad (on destination controller)
while in popover segue the invocation order is:
viewDidLoad (on destination controller)
prepareForSegue (on source controller)
just because I put all my logic in viewDidLoad, the controller was not properly initialized, and a crash happened. So this is not exactly true that prepareForSegue is not called, the truth is that I was getting an exception, and I wrongly mistaken as prepareForSegue not getting called.
I couldn't put everything in viewWillAppear because a call to CoreData had to be made and I didn't want to check if entities were ok each time the view display.
How did I solve this ? I created another method in destination controller
-(void)prepareViewController {
// initialization logic...
}
and changing the prepareForSegue method in source controller itself:
-(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
MyViewController *mvc = (MyViewController*)[segue destinationViewController];
// passing variable
// with segue style other than popover this called first than viewDidLoad
mvc.myProp1=#"prop1";
mvc.myProp2=#"prop2";
// viewWillAppear is not yet called
// so by sending message to controller
// the view is initialized
[mvc prepareViewController];
}
don't know if this is expected behavior with popover, anyway now things are working.
I've noticed that the boiler plate code for Xcode's Master-Detail template (iPhone) uses the following pattern for configuring the detail VC's view:
the detail VC's setters (for properties) are overwritten in order to invoke the configureView method (configureView would update all your controls in the view, e.g., labels, etc.)
the detail VC's viewDidLoad method also invokes the configureView method
I did not follow this pattern the other day when I was trying to re-use a detail VC in my movie app, and this gave me trouble.
I don't have much experience with popovers; however, if the pattern above is used with a detail VC that is displayed inside a popover, then wouldn't the detail VC's view get configured when you set the detail VC's properties from within the prepareForSegue method?