I have a view targeted for iPad that I present modally with modalPresentationStyle property set to UIModalPresentationFormSheet, by pushing firstly its view controller into a navigation controller:
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:myViewController];
navController.modalTransitionStyle = UIModalTransitionStyleFlipHorizontal;
navController.modalPresentationStyle = UIModalPresentationFormSheet;
[self presentViewController:navController animated:YES completion:nil];
Then, in the presented view controller, I want to detect tap gestures outside itself (as I said, I present it as a form sheet), so I have set a tap gesture this way:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
self.tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(handleTap:)];
self.tapGestureRecognizer.numberOfTouchesRequired = 1;
self.tapGestureRecognizer.numberOfTapsRequired = 1;
self.tapGestureRecognizer.cancelsTouchesInView = NO;
[self.view.window addGestureRecognizer:self.tapGestureRecognizer];
}
- (void)handleTap:(UITapGestureRecognizer*)sender
{
if (sender.state == UIGestureRecognizerStateEnded) {
CGPoint location = [sender locationInView:nil];
if (![self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil]) {
[self.view.window removeGestureRecognizer:sender];
[self dismissModalViewControllerAnimated:YES];
}
}
}
The navigation controller I'm presenting modally, and whose root view controller is the one setting this gesture recognizer, displays more views in hierarchy. When only the root view controller is pushed onto the navigation controller stack and I use the tap gesture outside it, it is correctly dismissed and I'm able to present modally this root view controller again. When I navigate from the root view controller and I push one more view controller onto the navigation stack, the tap gesture still works, but then the app crashes when I try to show the form sheet again.
How should I handle this gesture and the behavior I want having a navigation hierarchy?
Thanks in advance
I think your problem is removing the gesture recognizer with self.view.window -- that will be nil when another controller is pushed on the stack, since only the view on screen has a non-nil window property. Try replacing that with [UIApplication sharedApplication].delegate.window and see if that fixes the problem.
One of the possible cause of crash I've encountered is forgetting to declare your view controller as a childViewController to its parent controller.
Related
I've discovered that after dismissing a modal view controller that's presented fullscreen in a compact view and as a popover in a horizontally regular view, the screen goes black after the animation.
My View Hierarchy is the following
View (of my rootViewController on the window)
--->UISplitViewController.view ( set as a child viewController )
--------> rootViewController.view (set as the mainViewController of the splitView)
--------> detailViewController.view (set as the detailViewController of the split view)
Via the iPhone 6 simulator(split view is always collapsed) I present a modal viewcontroller with the following code:
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
[navigationController.navigationBar setBarStyle:UIBarStyleBlack];
[navigationController setModalPresentationStyle:UIModalPresentationPopover];
navigationController.popoverPresentationController.sourceView = view;
navigationController.popoverPresentationController.barButtonItem = barButtonItem;
navigationController.popoverPresentationController.delegate = self;
[self presentViewController:nav animated:YES completion:nil];
I dissmiss the presented controller from that viewController by calling:
[self dismissViewControllerAnimated:true completion:nil];
If I set animated to "false" I dont have any problems, but it looks bad and doesnt make sense.
I see some posts regarding this and custom presenatation methods, but I'm not using anything custom here.
On iPhone the ModalPresentationStyle should default to UIModalPresentationOverFullScreen, so I tried setting the presentationStyle directly to that, and it worked!
If I set the presentationStyle to "FullScreen" (not over fullscreen) I get the same behavior, a black screen after dismissing.
Any thoughts or experiences on this one?
I've come up with the following work around, but I still don't think it should be needed.
- (UIModalPresentationStyle)adaptivePresentationStyleForPresentationController:(UIPresentationController *)controller traitCollection:(UITraitCollection *)traitCollection{
if( traitCollection.horizontalSizeClass == UIUserInterfaceSizeClassCompact )
{
return UIModalPresentationOverFullScreen;
}
else
{
return UIModalPresentationPopover;
}
}
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.
I have a trio of functions and a property that I use to control my popovers as follows:
-(void)dismissPopoverIfPresentAnimated:(BOOL)animated
{
if (self.currentPopover)
{
[self.currentPopover dismissPopoverAnimated:animated];
self.currentPopover = nil;
}
}
-(void)presentViewController:(UIViewController *)viewController inView:(UIView *)view fromRect:(CGRect)rect suppressArrow:(BOOL)suppressArrow
{
//Did the user just tap on a button to bring up the same controller that's already displayed?
//If so, just dismiss the current controller.
BOOL closeOnly = NO;
if (self.currentPopover)
{
UIViewController *currentController = [self.currentPopover.contentViewController isKindOfClass:UINavigationController.class] ? ((UINavigationController *)self.currentPopover.contentViewController).topViewController : self.currentPopover.contentViewController;
UIViewController *newController = [viewController isKindOfClass:UINavigationController.class] ? ((UINavigationController *)viewController).topViewController : viewController;
if ([currentController isKindOfClass:newController.class])
closeOnly = YES;
[self dismissPopoverIfPresentAnimated:NO];
}
if (!closeOnly)
{
self.currentPopover = [[UIPopoverController alloc] initWithContentViewController:viewController];
self.currentPopover.backgroundColor = [UIColor whiteColor];
[self.currentPopover presentPopoverFromRect:rect inView:view permittedArrowDirections:(suppressArrow ? 0 : UIPopoverArrowDirectionAny) animated:YES];
}
}
(instancetype) initWithContentViewController:(UIViewController )viewController
{
self = [super initWithContentViewController:[[UIViewController alloc] init]];
if (self)
{
UIViewController contentViewController = super.contentViewController;
[contentViewController addChildViewController:viewController];
[viewController didMoveToParentViewController:contentViewController];
[contentViewController.view addSubview:viewController.view];
[self setPopoverContentSize:viewController.preferredContentSize animated:NO];
}
return self;
}
This runs fine in iOS 7, but in iOS 8 the problem is there is a delay between the call to presentPopoverFromRect and when the item actually shows up onscreen. So, if a user double taps a button to show a popover, the first tap will properly dismiss, then "start" the showing of the new controller. The second tap will make the dismiss call (the popover is not yet visible) and then not show the new controller (this is a design feature so that click a button will show a popover, clicking it again will hide it).
The problem is that the call to dismiss the popover doesn't actually work and the popover will show up. At that point I can't get rid of it because my property is nil and I think it is not showing.
My guess is this is an iOS 8 bug where the dismiss somehow doesn't see a visible popover and thus doesn't do anything, where instead, it should prevent it from showing up.
Oh, one last note is that the call to presentViewController is always done on the main thread.
I set up a mechanism where a modal view controller can be dismissed by tapping the outside of the view. The set up is as follows:
- (void)viewDidAppear:(BOOL)animated
{
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapBehind:)];
[recognizer setNumberOfTapsRequired:1];
recognizer.cancelsTouchesInView = NO; //So the user can still interact with controls in the modal view
[self.view.window addGestureRecognizer:recognizer];
}
- (void)handleTapBehind:(UITapGestureRecognizer *)sender
{
if (sender.state == UIGestureRecognizerStateEnded)
{
CGPoint location = [sender locationInView:nil]; //Passing nil gives us coordinates in the window
//Then we convert the tap's location into the local view's coordinate system, and test to see if it's in or outside. If outside, dismiss the view.
if (![self.view pointInside:[self.view convertPoint:location fromView:self.view.window] withEvent:nil])
{
[self dismissModalViewControllerAnimated:YES];
NSLog(#"There are %d Gesture Recognizers",[self.view.window gestureRecognizers].count);
[self.view.window removeGestureRecognizer:sender];
}
}
}
This works amazing for dismissing a single modal view. Now suppose I have two modal views, one called from within the root view controller (View A) and then another modal called from within the first modal (View B)
Kind of like this:
Root View -> View A -> View B
When I tap to dismiss View B, all is well. However I get an EXC_BAD_ACCESS error when I try to dismiss View A. After turning on zombies, it seems that View B is still getting the message handleTapBehind: sent to it, even though it's been dismissed and out of memory after View B was closed.
My question is why is View B still being messaged? (handleTapBehind: make sure that gesture recognizer should have been removed from the associated window.) And how can I get it to be sent to View A after View B is already dismissed.
PS. The code above appears both inside the controller for View A and for View B, and it is identical.
EDIT
Here's how I am calling the modal view controller, this code is inside a view controller that is within the standard view hierarchy.
LBModalViewController *vc = [[LBModalViewController alloc] initWithNibName:#"LBModalViewController" bundle:nil];
[vc.myTableView setDataSource:vc];
[vc setDataArray:self.object.membersArray];
[vc setModalPresentationStyle:UIModalPresentationFormSheet];
[vc setModalTransitionStyle:UIModalTransitionStyleFlipHorizontal];
[vc.view setClipsToBounds:NO];
[self presentViewController:vc animated:YES completion:nil];
// This is a hack to modify the size of the presented view controller
CGPoint modalOrigin = vc.view.superview.bounds.origin;
[[vc.view superview] setBounds:CGRectMake(modalOrigin.x, modalOrigin.y, 425, 351)];
[vc.view setBounds:CGRectMake(modalOrigin.x, modalOrigin.y, 425, 351)];
That's pretty much it, everything else is pretty standard.
[self dismissModalViewControllerAnimated:YES];
[self.view.window removeGestureRecognizer:sender];
Should be:
[self.view.window removeGestureRecognizer:sender];
[self dismissModalViewControllerAnimated:YES];
else you will get undefined results.
I have a view-based application with three xib files, each with its own view controllers. How do I change from one to another? I use this to move from xib 1 to xib 2, but when I use the same code to move from xib 2 to xib 1, i get a EXC_BAD_ACCESS on the [self presentModal....] line.
MapView *controller = [[MapView alloc] initWithNibName:#"MapView" bundle:nil];
controller.modalTransitionStyle = UIModalTransitionStyleCoverVertical;
[self presentModalViewController:controller animated:YES];
How can I freely move from one xib to another?
What I think you are trying to do is is present a modal view and then dismiss it, right? If that is the case then you put the code below in the method that you use to dismiss it(e.g. -(IBAction)dissmissModalView)
[self.parentViewController dismissModalViewControllerAnimated:YES];
Hopefully that works. Let me know.
initWithNibName isn't really necessary... you can change that to nil.
So, here is the correct code (without animation):
MapView *mapView = [[MapView alloc] initWithNibName:nil bundle:nil];
[self presentModalViewController:mapView animated:NO];
You should not be receiving EXC_BAD_ACCESS when trying to go back to view 1 using present. If you cannot resolve it, just use this instead:
[self dismissModalViewControllerAnimated:YES];
The second view controller will disappear and the first view controller will be visible again.
Note that presenting modal view controllers like the other answers here will mean that you have an ever-accumulating stack of view controllers. Use the application long enough and it will crash.
Instead, you can swap out the view from the application's window. Here's one way of doing that:
Add a data member to your app delegate to store the current view:
#class MyAppDelegate : NSObject <...>
{
UIViewController* currentVC;
}
and add a message there to swap VCs:
-(void)setCurrentVC:(UIViewController*)newVC
{
if (newVC==currentVC) return;
if (currentVC!=nil)
[currentVC.view removeFromSuperview];
currentVC = newVC;
if (newVC!=nil)
[self.window addSubview:newVC.view];
}
and to swap from one screen to another:
MapView* mapView = [[MapView alloc] init];
[[[UIApplication shared] delegate] setCurrentVC:mapView];