Switching between modal view controllers - ios

My app allows the user to switch between two different modal view controllers (for two different styles of data entry). The code below used to work (in iOS 4.3 and earlier):
UIViewController * parent = current.parentViewController;
[current dismissModalViewControllerAnimated:NO];
svc.modalPresentationStyle = UIModalPresentationFormSheet;
[parent presentModalViewController:svc animated:NO];
[svc release];
but no longer (in iOS 5) - the "current" view controller dismisses, but "svc" is not presented.
Any idea why it broke (i.e. what did I do wrong)?
Any idea how to do it "right" (so that it works on 5.0 as well as 4.3 and earlier)?

Jeff Hay was totally right in his comment except for one thing. You should do it in the -viewDidAppear: method of the view controller which originally presented the first modal view controller.
Example:
// MyViewController.h
#interface MyViewController : UIViewController {
BOOL _shouldPresentSecondModalViewController;
}
#end
// MyViewController.m
#implementation MyViewController
- (void)viewDidAppear:(BOOL)animated {
if(_shouldPresentSecondModalViewController) {
UINavigationController *myNavCon;
// Code to create second modal navigation controller
[self presentModalViewController:myNavCon animated:YES];
_shouldPresentSecondModalViewController = NO;
}
}
- (void)presentFirstViewController {
UINavigationController *myNavCon;
// Code to create the first navigation controller
_shouldPresentSecondModalViewController = YES;
[self presentModalViewController:myNavCon animated:YES];
}
#end
EDIT:
Now, if you want to pass data between the two modal view controllers, you can use a delegate.
// FirstModalViewControllerDelegate.h
#protocol FirstModalViewControllerDelegate
#optional
- (void)controller:(FirstModalViewControllerDelegate *)vc shouldShowData:(id)anyType;
#end
// MyViewController.h
#interface MyViewController : UIViewController <FirstModalViewControllerDelegate> {
id _dataToDisplay;
}
#end
// MyViewController.m
#implementation MyViewController
- (void)viewDidAppear:(BOOL)animated {
if(_dataToDisplay != nil) {
UINavigationController *myNavCon;
// Code to create second modal navigation controller
[self presentModalViewController:myNavCon animated:YES];
[_dataToDisplay release];
_dataToDisplay = nil;
}
}
- (void)presentFirstViewController {
UINavigationController *myNavCon;
FirstModalViewController *myCon;
// Code to create the first modal view controller
[myCon setDelegate:self];
myNavCon = [[UINavigationController alloc] initWithRootViewController:myCon];
[self presentModalViewController:myNavCon animated:YES];
[myNavCon release];
}
- (void)controller:(FirstModalViewControllerDelegate *)vc shouldShowData:(id)anyType {
/* This method will get called if the first modal view controller wants to display
some data. If the first modal view controller doesn't call this method, the
_dataToDisplay instance variable will stay nil. However, in that case, you'll of
course need to implement other methods to, like a response to a Done button, dismiss
the modal view controller */
[self dismissModalViewController];
_dataToDisplay = [anyType retain];
}
#end

Related

How do I push a view controller onto the nav stack from search results when presenting them modally?

I want to recreate the search UI shown in the iOS 7/8 calendar app. Presenting the search UI modally isn't a problem. I use UISearchController and modally present it just like the UICatalog sample code shows which gives me a nice drop down animation. The issue comes when trying to push a view controller from the results view controller. It isn't wrapped in a navigation controller so I can't push onto it. If I do wrap it in a navigation controller then I don't get the default drop down animation when I present the UISearchController. Any ideas?
EDIT:
I got it to push by wrapping my results view controller in a nav controller. However the search bar is still present after pushing the new VC onto the stack.
EDIT (2):
DTS from Apple said that the calendar app uses a non-standard method to push from search results. Instead they recommend removing focus from the search controller then pushing and returning focus on pop. This is similar to the way search in the settings app works I imagine.
Apple has gotten very clever there, but it's not a push, even though it looks like one.
They're using a custom transition (similar to what a navigation controller would do) to slide in a view controller which is embedded in a navigation controller.
You can spot the difference by slowly edge-swiping that detail view back and letting the previous view start to appear. Notice how the top navigation slides off to the right along with the details, instead of its bar buttons and title transitioning in-place?
Update:
The problem that you're seeing is that the search controller is presented above your navigation controller. As you discovered, even if you push a view controller onto a navigation controller's stack, the navigation bar is still beneath the search controller's presentation, so the search bar obscures any (pushed view controller's) navigation bar.
If you want to show results on top of the search controller without dismissing it, you'll need to present your own modal navigation view controller.
Unfortunately, there's no transition style which will let you present your navigation controller the same way the built-in push animation behaves.
As I can see, there are three effects that need to be duplicated.
The underlying content dims, as the presented view appears.
The presented view has a shadow.
The underlying content's navigation completely animates off-screen, but its content partially animates.
I've reproduced the general effect within an interactive custom modal transition. It generally mimic's Calendar's animation, but there are some differences (not shown), such as the keyboard (re)appearing too soon.
The modal controller that's presented is a navigation controller. I wired up a back button and edge swipe gesture to (interactively) dismiss it.
Here are the steps that are involved:
In your Storyboard, you would change the Segue type from Show Detail to Present Modally.
You can leave Presentation and Transition set to Default, as they'll need to be overridden in code.
In Xcode, add a new NavigationControllerDelegate file to your project.
NavigationControllerDelegate.h:
#interface NavigationControllerDelegate : NSObject <UINavigationControllerDelegate>
NavigationControllerDelegate.m:
#interface NavigationControllerDelegate () <UIViewControllerTransitioningDelegate>
#property (nonatomic, weak) IBOutlet UINavigationController *navigationController;
#property (nonatomic, strong) UIPercentDrivenInteractiveTransition* interactionController;
#end
- (void)awakeFromNib
{
UIScreenEdgePanGestureRecognizer *panGestureRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
panGestureRecognizer.edges = UIRectEdgeLeft;
[self.navigationController.view addGestureRecognizer:panGestureRecognizer];
}
#pragma mark - Actions
- (void)handlePan:(UIScreenEdgePanGestureRecognizer *)gestureRecognizer
{
UIView *view = self.navigationController.view;
if (gestureRecognizer.state == UIGestureRecognizerStateBegan)
{
if (!self.interactionController)
{
self.interactionController = [UIPercentDrivenInteractiveTransition new];
[self.navigationController dismissViewControllerAnimated:YES completion:nil];
}
}
else if (gestureRecognizer.state == UIGestureRecognizerStateChanged)
{
CGFloat percent = [gestureRecognizer translationInView:view].x / CGRectGetWidth(view.bounds);
[self.interactionController updateInteractiveTransition:percent];
}
else if (gestureRecognizer.state == UIGestureRecognizerStateEnded)
{
CGFloat percent = [gestureRecognizer translationInView:view].x / CGRectGetWidth(view.bounds);
if (percent > 0.5 || [gestureRecognizer velocityInView:view].x > 50)
{
[self.interactionController finishInteractiveTransition];
}
else
{
[self.interactionController cancelInteractiveTransition];
}
self.interactionController = nil;
}
}
#pragma mark - <UIViewControllerAnimatedTransitioning>
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)__unused presented presentingController:(UIViewController *)__unused presenting sourceController:(UIViewController *)__unused source
{
TransitionAnimator *animator = [TransitionAnimator new];
animator.appearing = YES;
return animator;
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)__unused dismissed
{
TransitionAnimator *animator = [TransitionAnimator new];
return animator;
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForPresentation:(id<UIViewControllerAnimatedTransitioning>)__unused animator
{
return nil;
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)__unused animator
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgnu-conditional-omitted-operand"
return self.interactionController ?: nil;
#pragma clang diagnostic pop
}
The delegate will provide the controller with its animator, interaction controller, and manage the screen edge pan gesture to dismiss the modal presentation.
In Storyboard, drag an Object (yellow cube) from the object library to the modal navigation controller. Set its class to ourNavigationControllerDelegate, and wire up its delegate and navigationController outlets to the storyboard's modal navigation controller.
In prepareForSegue from your search results controller, you'll need to set the modal navigation controller's transitioning delegate and modal presentation style.
navigationController.transitioningDelegate = (id<UIViewControllerTransitioningDelegate>)navigationController.delegate;
navigationController.modalPresentationStyle = UIModalPresentationCustom;
The custom animation that the modal presentation performs is handled by transition animator.
In Xcode, add a new TransitionAnimator file to your project.
TransitionAnimator.h:
#interface TransitionAnimator : NSObject <UIViewControllerAnimatedTransitioning>
#property (nonatomic, assign, getter = isAppearing) BOOL appearing;
TransitionAnimator.m:
#implementation TransitionAnimator
#synthesize appearing = _appearing;
#pragma mark - <UIViewControllerAnimatedTransitioning>
- (NSTimeInterval)transitionDuration:(id<UIViewControllerContextTransitioning>)transitionContext
{
return 0.3;
}
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
// Custom animation code goes here
}
The animation code is too long to provide within an answer, but it's available in a sample project which I've shared on GitHub.
Having said this, the code, as it stands, was more of a fun exercise. Apple has had years to refine and support all their transitions. If you adopt this custom animation, you may find cases (such as the visible keyboard) where the animation doesn't do what Apple's does. You'll have to decide whether you want to invest the time to improve the code to properly handle those cases.
I know this thread is old, but there seems to be a much simpler approach to getting the desired behavior.
The important thing to realize is the UISearchController is presented from the source controller, which is a view controller inside the navigation controller. If you inspect the view hierarchy, you see that the search controller, unlike regular modal presentations, isn't presented as a direct child of the window, but rather as a subview of the navigation controller.
So the general structure is
UINavigationController
MyRootViewController
UISearchViewController (presented pseudo-"modally")
MyContentController
Essentially you just need to get from the MyContentController up to the MyRootViewController, so you can access its navigationController property. In my tableView:didSelectRowAtIndexPath: method of my search content controller, I simply use the following to access my root view controller.
UINavigationController *navigationController = nil;
if ([self.parentViewController isKindOfClass:[UISearchController class]]) {
navigationController = self.parentViewController.presentingViewController.navigationController;
}
From there you can easily push something onto the navigation controller, and the animation is exactly what you'd expect.
EDIT: an alternate solution that doesn't rely on a UIWindow. I think the effect is very similar to the calendar app.
#interface SearchResultsController () <UINavigationControllerDelegate>
#end
#implementation SearchResultsController
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// this will be the UINavigationController that provides the push animation.
// its rootViewController is a placeholder that exists so we can actually push and pop
UIViewController* rootVC = [UIViewController new]; // this is the placeholder
rootVC.view.backgroundColor = [UIColor clearColor];
UINavigationController* nc = [[UINavigationController alloc] initWithRootViewController: rootVC];
nc.modalPresentationStyle = UIModalPresentationCustom;
nc.modalTransitionStyle = UIModalTransitionStyleCrossDissolve;
[UIView transitionWithView: self.view.window
duration: 0.25
options: UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionAllowAnimatedContent
animations: ^{
[self.parentViewController presentViewController: nc animated: NO completion: ^{
UIViewController* resultDetailViewController = [UIViewController alloc];
resultDetailViewController.title = #"Result Detail";
resultDetailViewController.view.backgroundColor = [UIColor whiteColor];
[nc pushViewController: resultDetailViewController animated: YES];
}];
}
completion:^(BOOL finished) {
nc.delegate = self;
}];
}
- (void) navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
// pop to root? then dismiss our window.
if ( navigationController.viewControllers[0] == viewController )
{
[UIView transitionWithView: self.view.window
duration: [CATransaction animationDuration]
options: UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionAllowAnimatedContent
animations: ^{
[self.parentViewController dismissViewControllerAnimated: YES completion: nil];
}
completion: nil];
}
}
#end
ORIGINAL solution:
Here's my solution. I start out using the same technique you discovered in the UICatalog example for showing the search controller:
- (IBAction)search:(id)sender
{
SearchResultsController* searchResultsController = [self.storyboard instantiateViewControllerWithIdentifier: #"SearchResultsViewController"];
self.searchController = [[UISearchController alloc] initWithSearchResultsController:searchResultsController];
self.searchController.hidesNavigationBarDuringPresentation = NO;
[self presentViewController:self.searchController animated:YES completion: nil];
}
In my example, SearchResultsController is a UITableViewController-derived class. When a search result is tapped it creates a new UIWindow with a root UINavigationController and pushes the result-detail view controller to that. It monitors for the UINavigationController popping to root so it can dismiss the special UIWindow.
Now, the UIWindow isn't strictly required. I used it because it helps keep the SearchViewController visible during the push/pop transition. Instead, you could just present the UINavigationController from the UISearchController (and dismiss it from the navigationController:didShowViewController: delegate method). But modally-presented view controllers present on an opaque view by default, hiding what's underneath. You could address this by writing a custom transition that would be applied as the UINavigationController's transitioningDelegate.
#interface SearchResultsController () <UINavigationControllerDelegate>
#end
#implementation SearchResultsController
{
UIWindow* _overlayWindow;
}
- (void) tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
// this will be the UINavigationController that provides the push animation.
// its rootViewController is a placeholder that exists so we can actually push and pop
UINavigationController* nc = [[UINavigationController alloc] initWithRootViewController: [UIViewController new]];
// the overlay window
_overlayWindow = [[UIWindow alloc] initWithFrame: self.view.window.frame];
_overlayWindow.rootViewController = nc;
_overlayWindow.windowLevel = self.view.window.windowLevel+1; // appear over us
_overlayWindow.backgroundColor = [UIColor clearColor];
[_overlayWindow makeKeyAndVisible];
// get this into the next run loop cycle:
dispatch_async(dispatch_get_main_queue(), ^{
UIViewController* resultDetailViewController = [UIViewController alloc];
resultDetailViewController.title = #"Result Detail";
resultDetailViewController.view.backgroundColor = [UIColor whiteColor];
[nc pushViewController: resultDetailViewController animated: YES];
// start looking for popping-to-root:
nc.delegate = self;
});
}
- (void) navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated
{
// pop to root? then dismiss our window.
if ( navigationController.viewControllers[0] == viewController )
{
[_overlayWindow resignKeyWindow];
_overlayWindow = nil;
}
}
#end
As you present a viewController the navigationController becomes unavailable. So you have to dismiss your modal first and then push another viewController.
UISearchController must be rootViewController of a UINavigationController and then you present navigation controller as modal.

Closing dismissViewController doesn't fire the viewWillAppear

I have a view which opens a modal seque View.
I open this view with this code:
- (IBAction)openInfopage:(UIButton *)sender;
{
[self performSegueWithIdentifier:#"Infopage" sender:self.view];
}
I close this View with:
- (IBAction)closeView:(id)sender {
// old version:
//[self dismissViewControllerAnimated:YES completion:nil];
// my new version:
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
When the modal is closed, I want to update the view with the new datas the modal view generated.
I use [self.tableView reloadData]; to update, but where can I put this code, so it will start?
I try the viewWillAppear, but this isn't called when I close the modal view.
I always put these things in a delegate-protocol. So just insert this to your modal-viewcontroller.h:
#protocol MyDelegateProtocol <NSObject>
#optional
-(void)modalFinished;
#end
#interface ViewController : UIViewController
#property (nonatomic, weak) id <MyDelegateProtocol> delegate;
#end
And in the .m call this:
if(_delegate) [_delegate modalFinished];
When you are done. In the presenting ViewController you have to omplement this:
<MyDelegateProtocol>
after the #implementation line. And implement the delegate-method -(void)modalFinished. In this method you can do your refresh. And of course set the delegate property from the modal-viewcontroller to self in the presenting view controller.
EDIT:
MyViewController mvc = ...;
MyViewController.delegate = self;
[self presentViewController:...];
and if you present via segue:
in prepareForSegue:
MyViewController *mvc = segue.destinationViewController;
mvc.delegate = self;

popToRootViewControllerAnimated does nothing, cannot get app top go to home sceen

I cannot get the [self.navigationController popToRootViewControllerAnimated:YES]; to work (Cannot get any of my apps to return back to home screen) . I tried creating a new blank test project and it still did not work, thus is what I did:
A. Used the wizard to create a single vue app
B. Added a UIViewController class through Xcode (add new file) called cHome
C. Added a button on the main storyboard
D. Added a Action for this by dragging and dropping called it a test
E. On the generated methed I added the following code:
- (IBAction)atest:(id)sender
{
if (mHome == nil)
{
mHome = [[cHome alloc] initWithNibName:#"cHome" bundle:[NSBundle mainBundle]];
}
[self presentModalViewController: mHome animated:NO];
}
G. I added a button to attest.xib
H. Added the - (IBAction)atest:(id)sender; to header and used UI to link it to button
I. Added following code to attest.m
- (IBAction)atest:(id)sender
{
[self.navigationController popToRootViewControllerAnimated:YES];
}
J. put a break point on code
K. when I run app, press button on first screen then button on next screen, breakpoint goes off, but app does not go back to pre screen
code
ViewController.h
#import <UIKit/UIKit.h>
#import "cHome.h"
#interface ViewController : UIViewController
{
cHome *mHome;
}
- (IBAction)atest:(id)sender;
#end
ViewController.m
#import "ViewController.h"
#implementation ViewController
- (IBAction)atest:(id)sender {
if ( mHome==nil)
{
mHome = [[ cHome alloc]
initWithNibName:#"cHome"
bundle:[NSBundle mainBundle]];
}
[self presentModalViewController: mHome animated:NO];
}
cHome.h
#import <UIKit/UIKit.h>
#interface cHome : UIViewController
- (IBAction)atest:(id)sender;
#end
cHome.m
#import "cHome.h"
#implementation cHome
- (IBAction)atest:(id)sender
{
[self.navigationController popToRootViewControllerAnimated:YES];
}
#end
Because the view controller was presented modally, you need to dismiss it, not pop to root, which would be appropriate if you had pushed the controller:
So replace this block of code:
- (IBAction)atest:(id)sender
{
[self.navigationController popToRootViewControllerAnimated:YES];
}
with this:
- (IBAction)atest:(id)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
Alternately, if you wanted to pop the view controller, you would have to push it first. So, make sure that ViewController is the root view controller of a UINaviagionController in your storyboard, then update the code as follows:
// In `ViewController.m`
- (IBAction)atest:(id)sender
{
if (mHome == nil)
{
mHome = [[cHome alloc] initWithNibName:#"cHome" bundle:[NSBundle mainBundle]];
}
[self.navigationController pushViewController:mHome animated:YES];
}
// In cHome.m
- (IBAction)atest:(id)sender
{
[self.navigationController popToRootViewControllerAnimated:YES];
}
if you use navigation control as a root view control in app delegate then this is enough to go home view
- (IBAction)atest:(id)sender
{
[self.navigationController popToRootViewControllerAnimated:YES];
}

Method not being called when view loads

I have a normal view controller with xib, m, and h file. I want when the view loads for it to automatically call a method. In my current M file I have the code call another view, this is just so I can see if the checkIfLogged method is working. When the app loads it doesn't call the other view it stays in its own view. How can I get the checkIfLogged method called when the viewloads? Actually I would prefer the method to be called before the view is even loaded if that is possible.
Here is my M file.
#import "ViewController.h"
#import "LoginView.h"
#interface ViewController ()
#end
#implementation ViewController
-(void) viewDidLoad{
[self checkIfLogged];
}
- (void) checkIfLogged
{
LoginView *loginView = [[LoginView alloc] initWithNibName:#"LoginView" bundle:nil];
[loginView setModalPresentationStyle:UIModalPresentationFormSheet]; //you can change the way it is presented
[loginView setModalTransitionStyle:UIModalTransitionStyleCoverVertical]; //you can change the animation
[self presentViewController:loginView animated:YES completion:nil]; //show the modal view
}//end checkIfLogged
#end
Here is my H file
#import <UIKit/UIKit.h>
#interface ViewController : UIViewController
-(IBAction)checkIfLogged;
#end
First, call [super viewDidLoad]; as the first line in your viewDidLoad implementation.
Second, you shouldn't attempt to present a view controller from viewDidLoad. Your UIViewController's view is not part of the view hierarchy at this point. Present the view controller from viewDidAppear: instead.

Delegation in Xcode

When the user pressed an 'add' button a modal view pops up for them to enter information. I have a 'cancel' button in the top left of a navigation bar and I want it to dismiss the current view controller when it is pressed. How do I set an object as the class's delegate? I understand creating protocols and implementing its methods but I cannot seem to make the delegate be set. When running the debugger my [self delegate] in the 'add' view controller is always nil.
Are you spawning the modal viewController through a segue set up in your Storyboard? If so, then in the prepareForSegue: method you would set the delegate there.
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
if([identifier isEqualToString:#"userGuideSegue_home"]){
UserGuideViewController* vc = segue.destinationViewController;
[[segue destinationViewController] setDelegate:self];
}
}
On the other hand, if you are setting up the modal viewController entirely through code, then you would create an instance of the modal viewController then set it's delegate.
- (void)showModelView:(NSString*)viewName
{
// code ripped out of project so a bit specific
if ([viewName isEqualToString:#"userGuide_name"]) {
modalViewController = (UserGuideViewController * )
[[UIStoryboard storyboardWithName:#"MainStoryboard_iPhone" bundle:NULL]
instantiateViewControllerWithIdentifier:#"UserGuide"];
}
modalViewController.delegate = self;
modalViewController.modalPresentationStyle = UIModalPresentationFormSheet;
modalViewController.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
modalViewController.view.frame = [[UIScreen mainScreen] applicationFrame];;
[self presentViewController:modalViewController
animated:YES
completion:^{
//put your code here
}];
}
Of course all this assumes you have defined a delegate property on your modal viewController.
If you created the view in IB, Control-drag the button into the ViewController's header file and add an IBOutlet. Inside of that method in the .m file you can
[self dismissModalViewControllerAnimated:YES];
alternatively you can create the button programmatically:
self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemCancel target:self action:#selector(processCancel:)];
-(void)processCancel:(UIBarButtonItem *)item{
[self dismissModalViewControllerAnimated:YES];
}
#interface MyViewController : UIViewController {
id delegate;
}
#property (nonatomic,retain) id delegate;
#synthesize delegate;
This should do the job, you can now use [MyViewController setDelegate:self] before showing the modal view and call [[self delegate] dismissModalViewControllerAnimated:YES] in the cancel button tapped event in MyViewController

Resources