Detailed explanation of custom transition (1)

foreword

This article is a little note I made after learning onevcat's introduction to this transition.

Today, let's implement a simple custom transition. Let's take a look at a rendering that this article will implement:
figure 1

Detailed process

warm up

We first create a project, first use storyboard to quickly create two controllers, one as the main controller, called ViewController, and the other as the presented controller, called PresentViewController, and use autoLayout to quickly build the interface. like this:
figure 2

We first do the logic of clicking the button above ViewController, presenting it PresentViewController, clicking PresentViewControllerthe button above, and dismissing it. PresentViewControllerThere are two points to note here:

  1. Because I use it here segue, when the ViewControllerbutton is clicked, we only need to call it like this.

        #pragma mark - 点我弹出
        -(IBAction)presentBtnClick:(UIButton *)sender {
            [self performSegueWithIdentifier:@"PresentSegue" sender:nil];
        }
  2. When we usually write dismiss, we usually send dismissViewControllerrelated methods directly to self in the second controller. In the current SDK, if the current VC is displayed, the message will be forwarded directly to the VC that displayed it. But this is not a good implementation, it violates the philosophy of programming, and it is easy to fall into the pit. So we do it in a standard delegateway dismiss.

First we PresentViewControllerdeclare a proxy method in the controller.

    #import <UIKit/UIKit.h>
    @class PresentViewController;
    @protocol PresentViewControllerDelegate <NSObject>
    - (void)dismissViewController:(PresentViewController *)viewController;
    @end
    @interface PresentViewController : UIViewController
    @property (nonatomic, weak) id<PresentViewControllerDelegate> delegate;
    @end

In the button's click event, let the delegate do the work of closing the current controller.

    #pragma mark - 点击关闭
    - (IBAction)closeBtnClick:(UIButton *)sender {
        if (self.delegate && [self.delegate respondsToSelector:@selector(dismissViewController:)]) {
            [self.delegate dismissViewController:self];
        }
    }

At the same time, ViewControlleryou need to set PresentViewControllerthe proxy in and implement the proxy method:

    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
        if ([segue.identifier isEqualToString:@"PresentSegue"]) {
            PresentViewController *presetVC = segue.destinationViewController;
            presetVC.delegate = self;
        }
    }
    #pragma mark - PresentViewControllerDelegate
    - (void)dismissViewController:(PresentViewController *)viewController {
        [self dismissViewControllerAnimated:YES completion:nil];
    }

OK, here, we have completed a basic transition (this is also an effect that comes with the system). like this:
image 3

main content

Next, to touch the main content we are going to talk about today, we use a new class in iOS7 UIViewControllerTransitioningto implement custom transitions.


UIViewControllerAnimatedTransitioning

First we need an UIViewControllerAnimatedTransitioningobject that implements the protocol named. Creating a class is called PresentAnimationinheriting from NSObjectand implementing the UIViewControllerAnimatedTransitioningprotocol. ( Note: UIKit framework needs to be imported )

    @interface PresentAnimation : NSObject<UIViewControllerAnimatedTransitioning>

This agreement is responsible for the specific content of the transition. When developers make custom switching effects, most of the code will be used to implement this protocol. There are only two methods that must be implemented in this protocol:

    // 返回动画的时间
    - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext;
    // 在进行切换的时候将调用该方法,我们对于切换时的UIView的设置和动画都在这个方法中完成。
    - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext;

implement these two methods

    - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
        return 0.8f;
    }
    - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
        // 1.我们需要得到参与切换的两个ViewController的信息,使用context的方法拿到它们的参照;
        UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];   
        // 2.对于要呈现的VC,我们希望它从屏幕下方出现,因此将初始位置设置到屏幕下边缘;
        CGRect finaRect = [transitionContext finalFrameForViewController:toVC];
        toVC.view.frame = CGRectOffset(finaRect, 0, [UIScreen mainScreen].bounds.size.height);
        // 3.将view添加到containerView中;
        [[transitionContext containerView] addSubview:toVC.view];
        // 4.开始动画。这里的动画时间长度和切换时间长度一致。usingSpringWithDamping的UIView动画API是iOS7新加入的,描述了一个模拟弹簧动作的动画曲线;
        [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 usingSpringWithDamping:0.6 initialSpringVelocity:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
            toVC.view.frame = finaRect;
        } completion:^(BOOL finished) {
            // 5.在动画结束后我们必须向context报告VC切换完成,是否成功。系统在接收到这个消息后,将对VC状态进行维护。
            [transitionContext completeTransition:YES];
        }];
    }

important point

UITransitionContextToViewControllerKeyAnd UITransitionContextFromViewControllerKey
For example, if B is presented from A, then A is FromViewController, B is ToViewController
If from B dismiss to A, then A is ToViewController, B isFromViewController

UIViewControllerTransitioningDelegate

The function of this interface is relatively simple. When VC switching is required, the system will ask the object that implements this interface whether to use a custom transition effect.
So, a better place is ViewControllerto implement this protocol directly in the main controller.

ViewControllerComplete the following code in :

    @interface ViewController ()<PresentViewControllerDelegate,UIViewControllerTransitioningDelegate>
    @property (nonatomic, strong) PresentAnimation *presentAnimation;
    @end
    @implementation ViewController
    #pragma mark - 懒加载
    - (PresentAnimation *)presentAnimation {
        if (!_presentAnimation) {
            _presentAnimation = [[PresentAnimation alloc] init];
        }
        return _presentAnimation;
    }
    #pragma mark - UIViewControllerTransitioningDelegate
    - (id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source {
        return self.presentAnimation;
    }
    - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
        if ([segue.identifier isEqualToString:@"PresentSegue"]) {
            PresentViewController *presetVC = segue.destinationViewController;
            presetVC.delegate = self;
            presetVC.transitioningDelegate = self;
        }
    }

Now let's take a look at our effect:
Figure 4
Compared with the effect of the above system, when we present the second controller, there is a spring effect.

Gesture-driven percentage toggle

Now we add a function, which is to use gesture swipe to dismiss. In layman's terms, it is to let the presented controller use gesture to dismiss.

  1. Create a class that inherits fromUIPercentDrivenInteractiveTransition

        #import <UIKit/UIKit.h>
        @interface PanInteractiveTransition : UIPercentDrivenInteractiveTransition
        -(void)panToDismiss:(UIViewController *)viewController;
        @end
    • We write a method for external class calls. Entry to the VC that allows external classes to see the incoming gesture dismiss.
  2. Since the VC that needs gesture dismiss is passed in, we need to save it so that the current class can be used elsewhere, so we create a new property to save the incoming VC.

        #import "PanInteractiveTransition.h"
        @interface PanInteractiveTransition ()
        @property (nonatomic, strong) UIViewController *presentVC;
        @end
        @implementation PanInteractiveTransition
        -(void)panToDismiss:(UIViewController *)viewController {
            self.presentVC = viewController;
            UIPanGestureRecognizer *panGestR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureAction:)];
            [self.presentVC.view addGestureRecognizer:panGestR];
        }
        #pragma mark - panGestureAction
        -(void)panGestureAction:(UIPanGestureRecognizer *)pan {
            CGPoint transition = [pan translationInView:self.presentVC.view];
            NSLog(@"%.2f",transition.y);
            switch (pan.state) {
                case UIGestureRecognizerStateBegan:{
                [self.presentVC dismissViewControllerAnimated:YES completion:nil];
            }
                break;
                case UIGestureRecognizerStateChanged:{  //
                    CGFloat percent = MIN(1.0, transition.y/300);
                    [self updateInteractiveTransition:percent];
                }
                    break;
                case UIGestureRecognizerStateCancelled:
                case UIGestureRecognizerStateEnded:{
                    if (pan.state == UIGestureRecognizerStateCancelled) {   // 手势取消
                        [self cancelInteractiveTransition];
                    }else{
                        [self finishInteractiveTransition];
                    }
                }
                    break;
                default:
                    break;
            }
        }
  3. As with creation PresentAnimation, we create a DismissAnimationclass

        @interface DismissAnimation : NSObject<UIViewControllerAnimatedTransitioning>
        @end
        @implementation DismissAnimation
        -(NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
            return 0.4f;
        }
        -(void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
            UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
            UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
            CGRect initRect = [transitionContext initialFrameForViewController:fromVC];
            CGRect finalRect = CGRectOffset(initRect, 0, [UIScreen mainScreen].bounds.size.height);
            UIView *contrainerView = [transitionContext containerView];
            [contrainerView addSubview:toVC.view];
            [contrainerView sendSubviewToBack:toVC.view];
            [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
                fromVC.view.frame = finalRect;
            } completion:^(BOOL finished) {
                [transitionContext completeTransition:YES];
            }];
        }
        @end
  4. Finally, we add a gesture-driven object to the main controller, a dismiss transition object, and lazy load.

        -(PanInteractiveTransition *)paninterTransition {
            if (!_paninterTransition) {
                _paninterTransition = [[PanInteractiveTransition alloc] init];
            }
            return _paninterTransition;
        }
        -(DismissAnimation *)dismissAnimation {
            if (!_dismissAnimation) {
                _dismissAnimation = [[DismissAnimation alloc] init];
            }
            return _dismissAnimation;
        }
        #pragma mark - UIViewControllerTransitioningDelegate
        -(id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed {
            return self.dismissAnimation;
        }
        -(id <UIViewControllerInteractiveTransitioning>)interactionControllerForDismissal:(id <UIViewControllerAnimatedTransitioning>)animator {
            return self.paninterTransition;
        }
        -(void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
            if ([segue.identifier isEqualToString:@"PresentSegue"]) {
                // ...
                [self.paninterTransition panToDismiss:presetVC];
            }
        }

Complete

At this point, when we run the program, we will find that although the above code can be gesture-driven, the function of clicking the button to dismiss cannot be used. This is because if you just return self.paninterTransition, the animation of clicking the button dismiss will be invalid; if you just return nil, then the effect of gesture sliding will be invalid . In summary, we consider the score situation.
Next we will improve it.

  1. Add an attribute to PanInteractiveTransitionindicate whether it is in the switching process (used to determine whether to click the button to dismiss or gesture-driven to dismiss)

        // 是否处于切换过程中
        @property (nonatomic, assign, getter=isInteracting) BOOL interacting;
  2. Add an attribute to PanInteractiveTransitionindicate whether dismiss is required (used to dismiss when the gesture exceeds the specified height, if not, it will be restored)

        @property (nonatomic, assign, getter=isShouldComplete) BOOL shouldComplete;
  3. Modified PanInteractiveTransitionmethod panGestureAction::

        -(void)panGestureAction:(UIPanGestureRecognizer *)pan {
            CGPoint transition = [pan translationInView:pan.view];
            switch (pan.state) {
                case UIGestureRecognizerStateBegan:{
                    self.interacting = YES;
                    [self.presentVC dismissViewControllerAnimated:YES completion:nil];
                }
                    break;
                case UIGestureRecognizerStateChanged:{  //
                    CGFloat percent = fmin(fmax(transition.y/300.0, 0.0), 1.0);
                    self.shouldComplete = (percent > 0.5);
                    [self updateInteractiveTransition:percent];
                }
                    break;
                case UIGestureRecognizerStateCancelled:
                case UIGestureRecognizerStateEnded:{
                    self.interacting = NO;
                    // 如果下移的距离小于300或者取消都当做取消
                    if (!self.isShouldComplete || pan.state == UIGestureRecognizerStateCancelled) {   // 手势取消
                        [self cancelInteractiveTransition];
                    }else{
                        [self finishInteractiveTransition];
                    }
                }
                    break;
                default:
                    break;
            }
        }
  4. Another point is that you need to modify DismissAnimationa piece of code:

        -(void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
            UIViewController *fromVC = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
            UIViewController *toVC = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];  
            CGRect initRect = [transitionContext initialFrameForViewController:fromVC];
            CGRect finalRect = CGRectOffset(initRect, 0, [UIScreen mainScreen].bounds.size.height);
            UIView *contrainerView = [transitionContext containerView];
            [contrainerView addSubview:toVC.view];
            [contrainerView sendSubviewToBack:toVC.view];
            [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{
                fromVC.view.frame = finalRect;
            } completion:^(BOOL finished) {
            // 此处做了修改,由之前的[transitionContext completeTransition:YES];
              [transitionContext completeTransition:![transitionContext transitionWasCancelled]];
            }];
        }

ok, so far, one of our custom transition animations is done.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324936032&siteId=291194637