How can I remove TabBar if I enter one given ViewController - ios

Setting:
Assume I have 2 TableViewControllers(All in their own NavigationControllers), which contain TypeA&B items correspondingly.
In any TableView, If I tap "+" button, it will segue to a Add[?]ItemViewController("?" is The Type of Item: A or B).So normally, even if I already in the AddView, I can also switch to another View By tapping Tab Bar Icon, right?
SO How can I inhibit user to switch if they already entered one AddView?
Use the Swift code? or just change the storyboard structure?
Here is the Structure of Main.storyboard:

We've done exactly the same in our application. To hide the default
TabBar, simply override the hidesBottomBarWhenPushed method in your
parent view controller (or in every view controller in your App)
#pragma mark - Overriden UIViewController methods
- (BOOL)hidesBottomBarWhenPushed {
return YES;
}
another Solution
You can also Hide Tab bar
// pass a param to describe the state change, an animated flag and a completion block matching UIView animations completion
- (void)setTabBarVisible:(BOOL)visible animated:(BOOL)animated completion:(void (^)(BOOL))completion {
// bail if the current state matches the desired state
if ([self tabBarIsVisible] == visible) return;
// get a frame calculation ready
CGRect frame = self.tabBarController.tabBar.frame;
CGFloat height = frame.size.height;
CGFloat offsetY = (visible)? -height : height;
// zero duration means no animation
CGFloat duration = (animated)? 0.3 : 0.0;
[UIView animateWithDuration:duration animations:^{
self.tabBarController.tabBar.frame = CGRectOffset(frame, 0, offsetY);
} completion:completion];
}
// know the current state
- (BOOL)tabBarIsVisible {
return self.tabBarController.tabBar.frame.origin.y < CGRectGetMaxY(self.view.frame);
}
// illustration of a call to toggle current state
- (IBAction)pressedButton:(id)sender {
[self setTabBarVisible:![self tabBarIsVisible] animated:YES completion:^(BOOL finished) {
NSLog(#"finished");
}];
}
another Solution
You can set the UIViewController.hidesBottomBarWhenPushed instead:
DetailViewController *detailViewController = [[DetailViewController alloc] init];
detailViewController.hidesBottomBarWhenPushed = YES;
[[self navigationController] pushViewController:detailViewController animated:YES];

Related

iOS - Switch between dismiss and scroll gestures

There's a behavior in the Line messenger app (the de facto messenger app in Japan) that I'm trying to emulate.
Basically, they have a modal view controller with a scroll view inside. When the scroll action reaches the top of its content, the view controller seamlessly switches to an interactive dismissal animation. Also, when the gesture returns the view to the top of the screen, control is returned to the scroll view.
Here's a gif of how it looks.
For the life of me, I can't figure out how they did it. I've tried a few different methods, but they've all failed, and I'm out of ideas. Can anyone point me in the right direction?
EDIT2
To clarify, the behavior that I want to emulate isn't just simply dragging the window down. I can do that, no problem.
I want to know how the same scroll gesture (without lifting the finger) triggers the dismissal transition and then transfers control back to the scroll view after the view has been dragged back to the original position.
This is the part that I can't figure out.
End EDIT2
EDIT1
Here's what I have so far. I was able to use the scroll view delegate methods to add a target-selector that handles the regular dismissal animation, but it still doesn't work as expected.
I create a UIViewController with a UIWebView as a property. Then I put it in a UINavigationController, which is presented modally.
The navigation controller uses animation/transition controllers for the regular interactive dismissal (which can be done by gesturing over the navigation bar).
From here, everything works fine, but the dismissal can't be triggered from the scroll view.
NavigationController.h
#interface NavigationController : UINavigationController <UIViewControllerTransitioningDelegate>
#property (nonatomic, strong) UIPanGestureRecognizer *gestureRecog;
- (void)handleGesture:(UIPanGestureRecognizer*)gestureRecognizer;
#end
NavigationController.m
#import "NavigationController.h"
#import "AnimationController.h"
#import "TransitionController.h"
#implementation NavigationController {
AnimationController *_animator;
TransitionController *_interactor;
}
- (instancetype)init {
self = [super init];
self.transitioningDelegate = self;
_animator = [[AnimationController alloc] init];
_interactor = [[TransitionController alloc] init];
return self;
}
- (void)viewDidLoad {
[super viewDidLoad];
// Set the gesture recognizer
self.gestureRecog = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handleGesture:)];
[self.view addGestureRecognizer:_gestureRecog];
}
- (id<UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id<UIViewControllerAnimatedTransitioning>)animator {
if (animator == _animator && _interactor.hasStarted) {
return _interactor;
}
return nil;
}
- (id<UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
if (dismissed == self || [self.viewControllers indexOfObject:dismissed] != NSNotFound) {
return _animator;
}
return nil;
}
- (void)handleGesture:(UIPanGestureRecognizer *)gestureRecog {
CGFloat threshold = 0.3f;
CGPoint translation = [gestureRecog translationInView:self.view];
CGFloat verticalMovement = translation.y / self.view.bounds.size.height;
CGFloat downwardMovement = fmaxf(verticalMovement, 0.0f);
CGFloat downwardMovementPercent = fminf(downwardMovement, 1.0f);
switch (gestureRecog.state) {
case UIGestureRecognizerStateBegan: {
_interactor.hasStarted = YES;
[self dismissViewControllerAnimated:YES completion:nil];
break;
}
case UIGestureRecognizerStateChanged: {
if (!_interactor.hasStarted) {
_interactor.hasStarted = YES;
[self dismissViewControllerAnimated:YES completion:nil];
}
_interactor.shouldFinish = downwardMovementPercent > threshold;
[_interactor updateInteractiveTransition:downwardMovementPercent];
break;
}
case UIGestureRecognizerStateCancelled: {
_interactor.hasStarted = NO;
[_interactor cancelInteractiveTransition];
break;
}
case UIGestureRecognizerStateEnded: {
_interactor.hasStarted = NO;
if (_interactor.shouldFinish) {
[_interactor finishInteractiveTransition];
} else {
[_interactor cancelInteractiveTransition];
}
break;
}
default: {
break;
}
}
}
#end
Now, I have to get that gesture handling to trigger when the scroll view has reached the top. So, here's what I did in the view controller.
WebViewController.m
#import "WebViewController.h"
#import "NavigationController.h"
#interface WebViewController ()
#property (weak, nonatomic) IBOutlet UIWebView *webView;
#end
#implementation WebViewController {
BOOL _isHandlingPan;
CGPoint _topContentOffset;
}
- (void)viewDidLoad {
[super viewDidLoad];
[self.webView.scrollView setDelegate:self];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if ((scrollView.panGestureRecognizer.state == UIGestureRecognizerStateBegan ||
scrollView.panGestureRecognizer.state == UIGestureRecognizerStateChanged) &&
! _isHandlingPan &&
scrollView.contentOffset.y < self.navigationController.navigationBar.translucent ? -64.0f : 0) {
NSLog(#"Adding scroll target");
_topContentOffset = CGPointMake(scrollView.contentOffset.x, self.navigationController.navigationBar.translucent ? -64.0f : 0);
_isHandlingPan = YES;
[scrollView.panGestureRecognizer addTarget:self action:#selector(handleGesture:)];
}
}
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
NSLog(#"Did End Dragging");
if (_isHandlingPan) {
NSLog(#"Removing action");
_isHandlingPan = NO;
[scrollView.panGestureRecognizer removeTarget:self action:#selector(handleGesture:)];
}
}
- (void)handleGesture:(UIPanGestureRecognizer*)gestureRecognizer {
[(NavigationController*)self.navigationController handleGesture:gestureRecognizer];
}
This still doesn't work quite right. Even during the dismissal animation, the scroll view is still scrolling with the gesture.
End EDIT1
That is a custom interactive transition.
First, you need set transitioningDelegate of UIViewController
id<UIViewControllerTransitioningDelegate> transitioningDelegate;
Then implment these two method to
//Asks your delegate for the transition animator object to use when dismissing a view controller.
- animationControllerForDismissedController:
//Asks your delegate for the interactive animator object to use when dismissing a view controller.
- interactionControllerForDismissal:
When drag to top, you start the transition, you may use UIPercentDrivenInteractiveTransition to control the progress during scrolling.
You can also refer to the source code of ZFDragableModalTransition
Image of ZFDragableModalTransition
As explained here the solution is quite complex. The person who answered, #trungduc, programmed a little demo published on github doing the sought behaviour. You can find it here.
The easiest way of making this work is to copy the 4 files found in /TestPanel/Presentation/ in the attached github repository, to your project. Then add the PanelAnimationControllerDelegate to your View Controller containing the scroll view (i.e. using the protocol).
Add the following to your View Controller, to satisfy the protocol:
func shouldHandlePanelInteractionGesture() -> Bool {
return (scrollView.contentOffset.y == 0);
}
Add this to deactivate the bouncing effect at the top:
func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollView.bounces = (scrollView.contentOffset.y > 10);
}
Set scrollView.delegate = self
Before presenting your View Controller containing the scroll view set the following propreties to your View Controller:
ScrollViewController.transitioningDelegate = self.panelTransitioningDelegate
ScrollViewController.modalPresentationStyle = .custom
If you want to change the size of your ScrollViewController, you will need to comment out the override of the frameOfPresentedViewInContainerView in the PanelPresentationController file (one of the 4). Then in the presentationTransitionWillBegin method, you will need to set let frameOfPresentedViewInContainerView = self.frameOfPresentedViewInContainerView.insetBy(dx: 0, dy: 20) with the wanted inset of dx and dy.
Thank you to trungduc for this amazing solution!!

UIContainerView Not responding to touch after event iOS

I'm trying to implement a drawer UIContainerView that allows the user to expand and contract it to reveal information. I have the container view set up and the button to "expand" is working fine.
However, once the view is 'expanded' it no longer responds to touch. It has a text view and two buttons. Even more; when I touch the screen it registers as a tap on the collectionView behind it, and NOT the buttons that I'm supposed to be able to interact with.
This is the code handling the parent / child relationship with the container view:
// Parent View Controller.h
{
UIViewController *child1;
profileDescriptionViewController *profileDescription;
}
// Parent View Controller.m
- (void)viewDidLoad
{
child1 = [self.storyboard instantiateViewControllerWithIdentifier:#"profileDescription"];
[child1 didMoveToParentViewController:self];
}
// Child View Controller.m
- (void)viewDidLoad
{
_profileDescription.text = [[dataHandler getAthlete] profileDescriptionString];
_isExposed = NO;
_frame = self.view.frame;
_height = _frame.size.height;
_yOrigin = _frame.origin.y;
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (IBAction)profileExpand:(id)sender
{
if (_isExposed == NO) {
[UIView animateWithDuration:0.5 animations:^
{
//0, 487, 320, 32
[self.view setFrame:CGRectMake(_frame.origin.x, _yOrigin - 100, 320, _yOrigin + 150)];
}
completion:^(BOOL finished)
{
[self.view setUserInteractionEnabled:YES];
_isExposed = YES;
}];
}
/* else
{
[UIView animateWithDuration:0.5 animations:^
{
[self.view setFrame:CGRectMake(_frame.origin.x, _yOrigin + 100, 320, _yOrigin - 150)];
}
completion:^(BOOL finished)
{
_isExposed = NO;
}];
}*/
}
I just want the view to respond to touch events. It's not being placed behind any UI elements on the parentViewController but I can't select either button at the top or the description, and instead it interacts with elements behind it.
Anyone have any ideas as to why this would be? In storyboard its set up like a regular container view.
I met a similar problem, the reason is the super view is not big enough, so after expand the subview, you can see it, but it not responding to any touch event. You can set the clipsToBound of the super view to NO to check it. Hope this helps.

Keyboard handling just like in Messages app in iOS 7

I am implementing a view that is in some way similar to what happens in Messages app, so there is a view with UITextView attached to the bottom of the screen and there is also UITableView showing the main content. When it is tapped it slides up with the keyboard and when keyboard is dismissed it slides back to the bottom of the screen.
That part I have and it is working perfectly - I just subscribed to keyboard notifications - will hide and wil show.
The problem is that I have set keyboard dismiss mode on UITableView to interactive and I cannot capture changes to keyboard when it is panning.
The second problem is that this bar with uitextview is covering some part of uitableview. How to fix this? I still want the uitableview to be "under" this bar just like in messages app.
I am using AutoLayout in all places.
Any help will be appreciated!
============
EDIT1:
Here is some code:
View Hierarchy is as follows:
View
- UITableView (this one will contain "messages")
- UIView (this one will slide)
UITableView is has constraints to top, left, right and bottom of parent view so it fills whole screen.
UIView has constraints to left, right and bottom of parent view so it is glued to the bottom - I moved it by adjusting constant on constraint.
In ViewWillAppear method:
NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.DidShowNotification, OnKeyboardDidShowNotification);
NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.WillChangeFrameNotification, OnKeyboardDidShowNotification);
NSNotificationCenter.DefaultCenter.AddObserver (UIKeyboard.WillHideNotification, OnKeyboardWillHideNotification);
And here are methods:
void OnKeyboardDidShowNotification (NSNotification notification)
{
AdjustViewToKeyboard (Ui.KeyboardHeightFromNotification (notification), notification);
}
void OnKeyboardWillHideNotification (NSNotification notification)
{
AdjustViewToKeyboard (0.0f, notification);
}
void AdjustViewToKeyboard (float offset, NSNotification notification = null)
{
commentEditViewBottomConstraint.Constant = -offset;
if (notification != null) {
UIView.BeginAnimations (null, IntPtr.Zero);
UIView.SetAnimationDuration (Ui.KeyboardAnimationDurationFromNotification (notification));
UIView.SetAnimationCurve ((UIViewAnimationCurve)Ui.KeyboardAnimationCurveFromNotification (notification));
UIView.SetAnimationBeginsFromCurrentState (true);
}
View.LayoutIfNeeded ();
commentEditView.LayoutIfNeeded ();
var insets = commentsListView.ContentInset;
insets.Bottom = offset;
commentsListView.ContentInset = insets;
if (notification != null) {
UIView.CommitAnimations ();
}
}
I'd recommend you to override -inputAccessoryView property of your view controller and have your editable UITextView as its subview.
Also, don't forget to override -canBecomeFirstResponder method to return YES.
- (BOOL)canBecomeFirstResponder
{
if (!RUNNING_ON_IOS7 && !RUNNING_ON_IPAD)
{
//Workaround for iOS6-specific bug
return !(self.viewDisappearing) && (!self.viewAppearing);
}
return !(self.viewDisappearing);
}
With this approach system manages everything.
There are also some workarounds you must know about: for UISplitViewController (UISplitViewController detail-only inputAccessoryView), for deallocation bugs (UIViewController with inputAccessoryView is not deallocated) and so on.
This solution is based on a lot of different answers on SO. It have a lot of benefits:
Compose bar stays on bottom when keyboard is hidden
Compose bas follows keyboard while interactive gesture on UITableView
UITableViewCells are going from bottom to top, like in Messages app
Keyboard do not prevent to see all UITableViewCells
Should work for iOS6, iOS7 and iOS8
This code just works:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = // . . .
// . . .
cell.contentView.transform = CGAffineTransformMakeScale(1,-1);
cell.accessoryView.transform = CGAffineTransformMakeScale(1,-1);
return cell;
}
- (UIView *)inputAccessoryView {
return self.composeBar;
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.tableView.transform = CGAffineTransformMakeScale(1,-1);
// This code prevent bottom inset animation while appearing view
UIEdgeInsets newEdgeInsets = self.tableView.contentInset;
newEdgeInsets.top = CGRectGetMaxY(self.navigationController.navigationBar.frame);
newEdgeInsets.bottom = self.view.bounds.size.height - self.composeBar.frame.origin.y;
self.tableView.contentInset = newEdgeInsets;
self.tableView.scrollIndicatorInsets = newEdgeInsets;
self.tableView.contentOffset = CGPointMake(0, -newEdgeInsets.bottom);
// This code need to be done if you added compose bar via IB
self.composeBar.delegate = self;
[self.composeBar removeFromSuperview];
[[NSNotificationCenter defaultCenter] addObserverForName:UIKeyboardWillChangeFrameNotification object:nil queue:nil usingBlock:^(NSNotification *note)
{
NSNumber *duration = note.userInfo[UIKeyboardAnimationDurationUserInfoKey];
NSNumber *options = note.userInfo[UIKeyboardAnimationCurveUserInfoKey];
CGRect beginFrame = [note.userInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue];
CGRect endFrame = [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
UIEdgeInsets newEdgeInsets = self.tableView.contentInset;
newEdgeInsets.bottom = self.view.bounds.size.height - endFrame.origin.y;
CGPoint newContentOffset = self.tableView.contentOffset;
newContentOffset.y += endFrame.origin.y - beginFrame.origin.y;
[UIView animateWithDuration:duration.doubleValue
delay:0.0
options:options.integerValue << 16
animations:^{
self.tableView.contentInset = newEdgeInsets;
self.tableView.scrollIndicatorInsets = newEdgeInsets;
self.tableView.contentOffset = newContentOffset;
} completion:^(BOOL finished) {
;
}];
}];
}
Use for example pod 'PHFComposeBarView' compose bar:
#property (nonatomic, strong) IBOutlet PHFComposeBarView *composeBar;
And use this class for your table view:
#interface InverseTableView : UITableView
#end
#implementation InverseTableView
void swapCGFLoat(CGFloat *a, CGFloat *b) {
CGFloat tmp = *a;
*a = *b;
*b = tmp;
}
- (UIEdgeInsets)contentInset {
UIEdgeInsets insets = [super contentInset];
swapCGFLoat(&insets.top, &insets.bottom);
return insets;
}
- (void)setContentInset:(UIEdgeInsets)contentInset {
swapCGFLoat(&contentInset.top, &contentInset.bottom);
[super setContentInset:contentInset];
}
#end
If you would like keyboard to disappear by tapping on message:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.composeBar.textView resignFirstResponder];
}
Do not call this, this will hide composeBar at all:
[self resignFirstResponder];
UPDATE 2:
NEW SOLUTION for keyboard tracking works much better:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
// Compose view height growing tracking
[self.composeBar addObserver:self forKeyPath:#"frame" options:0 context:nil];
// iOS 7 keyboard tracking
[self.composeBar.superview addObserver:self forKeyPath:#"center" options:0 context:nil];
// iOS 8 keyboard tracking
[self.composeBar.superview addObserver:self forKeyPath:#"frame" options:0 context:nil];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[self.composeBar removeObserver:self forKeyPath:#"frame"];
[self.composeBar.superview removeObserver:self forKeyPath:#"center"];
[self.composeBar.superview removeObserver:self forKeyPath:#"frame"];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == self.composeBar.superview || object == self.composeBar)
{
// Get all values
CGPoint newContentOffset = self.tableView.contentOffset;
UIEdgeInsets newEdgeInsets = self.tableView.contentInset;
UIEdgeInsets newScrollIndicartorInsets = self.tableView.scrollIndicatorInsets;
// Update values
CGFloat bottomInset = self.view.bounds.size.height - [self.composeBar convertPoint:CGPointZero toView:self.view].y;
CGFloat diff = newEdgeInsets.bottom - (bottomInset + 7);
newContentOffset.y += diff;
newEdgeInsets.bottom = bottomInset + 7;
newScrollIndicartorInsets.bottom = bottomInset;
// Set all values
if (diff < 0 || diff > 40)
self.tableView.contentOffset = CGPointMake(0, newContentOffset.y);
self.tableView.contentInset = newEdgeInsets;
self.tableView.scrollIndicatorInsets = newEdgeInsets;
}
}
OK, the interactive keyboard dismissal will send a notification with name UIKeyboardDidChangeFrameNotification.
This can be used to move the text view while the keyboard is being dismissed interactively.
You are already using this but you are sending it to the OnKeyboardDidShow method.
You need a third method called something like keyboardFramedDidChange. This works for the hide and the show.
For the second problem, you should have your vertical constraints like this...
|[theTableView][theTextView (==44)]|
This will tie the bottom of the tableview to the top of the text view.
This doesn't change how any of the animation works it will just make sure that the table view will show all of its contents whether the keyboard is visible or not.
Don't update the content insets of the table view. Use the constraints to make sure the frames do not overlap.
P.S. sort out your naming conventions. Method names start with a lowercase letter.
P.P.S. use block based animations.
I'd try to use an empty, zero-height inputAccessoryView. The trick is to glue your text field's bottom to it when the keyboard appears, so that they'd move together. When the keyboard is gone, you can destroy that constraint and stick to the bottom of the screen once again.
I made an open source lib for exactly this purpose. It works on iOS 7 and 8 and is set up to work as a cocoapod as well.
https://github.com/oseparovic/MessageComposerView
Here's a sample of what it looks like:
You can use a very basic init function as shown below to create it with screen width and default height e.g.:
self.messageComposerView = [[MessageComposerView alloc] init];
self.messageComposerView.delegate = self;
[self.view addSubview:self.messageComposerView];
There are several other initializers that are also available to allow you to customize the frame, keyboard offset and textview max height as well as some delegates to hook into frame changes and button clicks. See readme for more!

Keep UIKeyboard with view when swiping back iOS 7

I have a view controller that can be popped with the new interactivePopGestureRecognizer. If there is a keyboard present and the swipe animation begins the keyboard does not move with the view. I have had a look at this question and implemented it like this in my view controller that gets dismissed
-(void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.transitionCoordinator animateAlongsideTransitionInView:self.aTextInputView.keyboardSuperView animation:^(id<UIViewControllerTransitionCoordinatorContext> context) {
CGRect frame = self.aTextInputView.keyboardSuperView.frame;
frame.origin.x = self.view.frame.size.width;
self.aTextInputView.keyboardSuperView.frame = frame;
} completion:nil];
}
Now what I get when the view animates to disappear is the keyboard animates off the screen to the x point of 320 which makes sense as thats what I set it to, my question is how do I get the keyboard to animate with the swipe back?
Update
For any one that sees a weird animation when the view disappears you can get remove the keyboard by doing this.
[self.transitionCoordinator notifyWhenInteractionEndsUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context){
if (![context isCancelled]) {
[keyboardSuperview removeFromSuperview];
}
}];
You have a lot of custom code in your snippet, so correct me if I am wrong, but it seems you have incorrect self.aTextInputView.keyboardSuperView.
Double check that it is not nil. If it is, you forgot to add an inputAccessoryView.
Here is the full code snippet without any extensions:
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
UIView *keyboardSuperview = self.textField.inputAccessoryView.superview;
[self.transitionCoordinator animateAlongsideTransitionInView:keyboardSuperview
animation:
^(id<UIViewControllerTransitionCoordinatorContext> context) {
CGRect keyboardFrame = keyboardSuperview.frame;
keyboardFrame.origin.x = self.view.bounds.size.width;
keyboardSuperview.frame = keyboardFrame;
}
completion:nil];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.textField.inputAccessoryView = [[UIView alloc] init];
}
Just found really simple solution for iOS8
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.aTextInputView resignFirstResponder];
}

Navigating UIViewControllers with gestures in iOS

I have several view controllers embedded in a UINavigationController (some modal, some pushed) and am navigating through them using swipe gestures as such:
// Gesture recognizers
UISwipeGestureRecognizer *downGesture = [[UISwipeGestureRecognizer alloc]initWithTarget:self action:#selector(dismissButton)];
downGesture.direction = UISwipeGestureRecognizerDirectionDown;
[downGesture setCancelsTouchesInView:NO];
[self.view addGestureRecognizer:downGesture];
This works fine, however I want the user to be able to physically drag the modally presented view controller, for example, down and off the screen instead of just a flick and an animation doing the rest, or dragging right across the screen and snapping to the previous view instead of tapping the back button.
I've tried implementing this using a pan gesture on the view but of course the previous view controller isn't visible behind it, which it needs to be. How is this effect achieved properly? With view controller containment? If so how would that work when pushing a few view controllers on to the stack? An example of the type of navigation I'm talking about can be found in the LetterPress app.
thanks.
Yes, custom container view is the way to go (iOS 5 and greater). You basically write your own custom container, using the built-in childViewControllers property to keep track of all of the child view controllers. You may want your own property, say currentChildIndex, to keep track of which child controller you're currently on:
#property (nonatomic) NSInteger currentChildIndex;
Your parent controller should probably have some push and pop methods for non-swipe related navigation, such as:
- (void)pushChildViewController:(UIViewController *)newChildController
{
// remove any other children that we've popped off, but are still lingering about
for (NSInteger index = [self.childViewControllers count] - 1; index > self.currentChildIndex; index--)
{
UIViewController *childController = self.childViewControllers[index];
[childController willMoveToParentViewController:nil];
[childController.view removeFromSuperview];
[childController removeFromParentViewController];
}
// get reference to the current child controller
UIViewController *currentChildController = self.childViewControllers[self.currentChildIndex];
// set new child to be off to the right
CGRect frame = self.containerView.bounds;
frame.origin.x += frame.size.width;
newChildController.view.frame = frame;
// add the new child
[self addChildViewController:newChildController];
[self.containerView addSubview:newChildController.view];
[newChildController didMoveToParentViewController:self];
[UIView animateWithDuration:0.5
animations:^{
CGRect frame = self.containerView.bounds;
newChildController.view.frame = frame;
frame.origin.x -= frame.size.width;
currentChildController.view.frame = frame;
}];
self.currentChildIndex++;
}
- (void)popChildViewController
{
if (self.currentChildIndex == 0)
return;
UIViewController *currentChildController = self.childViewControllers[self.currentChildIndex];
self.currentChildIndex--;
UIViewController *previousChildController = self.childViewControllers[self.currentChildIndex];
CGRect onScreenFrame = self.containerView.bounds;
CGRect offToTheRightFrame = self.containerView.bounds;
offToTheRightFrame.origin.x += offToTheRightFrame.size.width;
[UIView animateWithDuration:0.5
animations:^{
currentChildController.view.frame = offToTheRightFrame;
previousChildController.view.frame = onScreenFrame;
}];
}
Personally, I have a protocol defined for these two methods, and make sure that my parent controller is configured to conform to that protocol:
#protocol ParentControllerDelegate <NSObject>
- (void)pushChildViewController:(UIViewController *)newChildController;
- (void)popChildViewController;
#end
#interface ParentViewController : UIViewController <ParentControllerDelegate>
...
#end
Then, when a child wants to push a new child on, it can do it like so:
ChildViewController *controller = ... // instantiate and configure your next controller however you want to do that
id<ParentControllerDelegate> parent = (id)self.parentViewController;
NSAssert([parent conformsToProtocol:#protocol(ParentControllerDelegate)], #"Parent must conform to ParentControllerDelegate");
[parent pushChildViewController:controller];
When a child wants to pop itself off, it can do it like so:
id<ParentControllerDelegate> parent = (id)self.parentViewController;
NSAssert([parent conformsToProtocol:#protocol(ParentControllerDelegate)], #"Parent must conform to ParentControllerDelegate");
[parent popChildViewController];
And then the parent view controller has a pan gesture set up, to handle the user panning from one child to another:
- (void)handlePan:(UIPanGestureRecognizer *)gesture
{
static UIView *currentView;
static UIView *previousView;
static UIView *nextView;
if (gesture.state == UIGestureRecognizerStateBegan)
{
// identify previous view (if any)
if (self.currentChildIndex > 0)
{
UIViewController *previous = self.childViewControllers[self.currentChildIndex - 1];
previousView = previous.view;
}
else
{
previousView = nil;
}
// identify next view (if any)
if (self.currentChildIndex < ([self.childViewControllers count] - 1))
{
UIViewController *next = self.childViewControllers[self.currentChildIndex + 1];
nextView = next.view;
}
else
{
nextView = nil;
}
// identify current view
UIViewController *current = self.childViewControllers[self.currentChildIndex];
currentView = current.view;
}
// if we're in the middle of a pan, let's adjust the center of the views accordingly
CGPoint translation = [gesture translationInView:gesture.view.superview];
previousView.transform = CGAffineTransformMakeTranslation(translation.x, 0.0);
currentView.transform = CGAffineTransformMakeTranslation(translation.x, 0.0);
nextView.transform = CGAffineTransformMakeTranslation(translation.x, 0.0);
// if we're all done, let's animate the completion (or if we didn't move far enough,
// the reversal) of the pan gesture
if (gesture.state == UIGestureRecognizerStateEnded ||
gesture.state == UIGestureRecognizerStateCancelled ||
gesture.state == UIGestureRecognizerStateFailed)
{
CGPoint center = currentView.center;
CGPoint currentCenter = CGPointMake(center.x + translation.x, center.y);
CGPoint offRight = CGPointMake(center.x + currentView.frame.size.width, center.y);
CGPoint offLeft = CGPointMake(center.x - currentView.frame.size.width, center.y);
CGPoint velocity = [gesture velocityInView:gesture.view.superview];
if ((translation.x + velocity.x * 0.5) < (-self.containerView.frame.size.width / 2.0) && nextView)
{
// if we finished pan to left, reset transforms
previousView.transform = CGAffineTransformIdentity;
currentView.transform = CGAffineTransformIdentity;
nextView.transform = CGAffineTransformIdentity;
// set the starting point of the animation to pick up from where
// we had previously transformed the views
CGPoint nextCenter = CGPointMake(nextView.center.x + translation.x, nextView.center.y);
currentView.center = currentCenter;
nextView.center = nextCenter;
// and animate the moving of the views to their final resting points,
// adjusting the currentChildIndex appropriately
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
currentView.center = offLeft;
nextView.center = center;
self.currentChildIndex++;
}
completion:NULL];
}
else if ((translation.x + velocity.x * 0.5) > (self.containerView.frame.size.width / 2.0) && previousView)
{
// if we finished pan to right, reset transforms
previousView.transform = CGAffineTransformIdentity;
currentView.transform = CGAffineTransformIdentity;
nextView.transform = CGAffineTransformIdentity;
// set the starting point of the animation to pick up from where
// we had previously transformed the views
CGPoint previousCenter = CGPointMake(previousView.center.x + translation.x, previousView.center.y);
currentView.center = currentCenter;
previousView.center = previousCenter;
// and animate the moving of the views to their final resting points,
// adjusting the currentChildIndex appropriately
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
currentView.center = offRight;
previousView.center = center;
self.currentChildIndex--;
}
completion:NULL];
}
else
{
[UIView animateWithDuration:0.25
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
previousView.transform = CGAffineTransformIdentity;
currentView.transform = CGAffineTransformIdentity;
nextView.transform = CGAffineTransformIdentity;
}
completion:NULL];
}
}
}
It looks like you're doing up and down panning, rather than the left-right panning that I used above, but hopefully you get the basic idea.
By the way, in iOS 6, the user interface you're asking about (the sliding between views using gestures), could probably be done more efficiently using a built-in container controller, UIPageViewController. Just use a transition style of UIPageViewControllerTransitionStyleScroll and a navigation orientation of UIPageViewControllerNavigationOrientationHorizontal. Unfortunately, iOS 5 only allows page curl transitions, and Apple only introduced the scrolling transitions that you want in iOS 6, but if that's all you need, UIPageViewController gets the job done even more efficiently than what I've laid out above (you don't have to do any custom container calls, no writing of gesture recognizers, etc).
For example, you can drag a "page view controller" onto your storyboard, create a UIPageViewController subclass and then in viewDidLoad, you need to configure the first page:
UIViewController *firstPage = [self.storyboard instantiateViewControllerWithIdentifier:#"1"]; // use whatever storyboard id your left page uses
self.viewControllerStack = [NSMutableArray arrayWithObject:firstPage];
[self setViewControllers:#[firstPage]
direction:UIPageViewControllerNavigationDirectionForward
animated:NO
completion:NULL];
self.dataSource = self;
Then you need to define the following UIPageViewControllerDataSource methods:
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
{
if ([viewController isKindOfClass:[LeftViewController class]])
return [self.storyboard instantiateViewControllerWithIdentifier:#"2"];
return nil;
}
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
{
if ([viewController isKindOfClass:[RightViewController class]])
return [self.storyboard instantiateViewControllerWithIdentifier:#"1"];
return nil;
}
Your implementation will vary (at the very least different class names and different storyboard identifiers; I'm also letting the page view controller instantiate the next page's controller when the user asks for it and because I'm not retaining any strong reference to them, the'll be released when I'm done transitioning to the other page ... you could alternatively just instantiate both at startup and then these before and after routines would obviously not instantiate, but rather look them up in an array), but hopefully you get the idea.
But the key issue is that I don't have any gesture code, no custom container view controller code, etc. Much simpler.

Resources