封装了一个优雅的iOS全屏侧滑返回工具

发布于:2025-04-01 ⋅ 阅读:(24) ⋅ 点赞:(0)

思路
添加一个全屏返回手势,UIPangesturerecognizer,
1 手势开始
在手势开始响应的时候,将navigationController的delegate代理设置为工具类,在工具类中执行代理方法,- (nullable id )navigationController:(UINavigationController *)navigationController
和 - (nullable id )navigationController:(UINavigationController *)navigationController

都返回为侧滑工具对象,然后侧滑工具实现

  • (NSTimeInterval)transitionDuration:(nullable id )transitionContext {
    和 - (void)animateTransition:(id)transitionContext
    方法 , 并实现 - (void)startInteractiveTransition:(id )transitionContext {
    方法,

2 手势改变
计算滑动比例,我们以超过200就算1,根据translationInView 计算滑动比例

在手势的改变的状态下,
通过 UIView *toView = [self.transitionContext viewForKey:UITransitionContextToViewKey];
UIView *fromView = [self.transitionContext viewForKey:UITransitionContextFromViewKey];
UIView *containerView = self.transitionContext.containerView;
方法获取到要退回到的视图和来源视图,并修改frame,
达到两个视图移动的效果

3 手势结束,根据滑动距离和速度确定是完成返回还是取消返回

4 注意要添加弹簧动画,使交互效果更流畅优雅

效果图

请添加图片描述

代码如下

@interface LBTransitionManager : NSObject

@property (nonatomic, weak) UINavigationController *navigationController;

+ (instancetype)shareManager;

- (UIPanGestureRecognizer *)addPanGestureRecognizerToViewControllerIfNeeded:(UIViewController *)viewController ;

@end

//
//  LBTransitionManager.m
//  TEXT
//
//  Created by mac on 2025/3/30.
//

#import "LBTransitionManager.h"
#import <UIKit/UIKit.h>
#import "LBTransitionInteractivePopLinearAnimation.h"

@interface LBTransitionManager () <UINavigationControllerDelegate,
UIViewControllerAnimatedTransitioning,
UIViewControllerInteractiveTransitioning>

@property (nonatomic, strong) LBTransitionInteractivePopLinearAnimation *currentInteractiveAnimation;

@property (nonatomic, weak) UIPanGestureRecognizer *currentPanGestureRecognizer;

@property (nonatomic, strong) id <UIViewControllerContextTransitioning> transitionContext;

@end

@implementation LBTransitionManager

+ (instancetype)shareManager
{
    static dispatch_once_t onceToken;
    static LBTransitionManager *shareManager = nil;
    dispatch_once(&onceToken, ^{
        shareManager = [[self alloc] init];
    });
    return shareManager;;
}

- (UIPanGestureRecognizer *)addPanGestureRecognizerToViewControllerIfNeeded:(UIViewController *)viewController {
    UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
    [viewController.view addGestureRecognizer:panGestureRecognizer];
    return panGestureRecognizer;
}

#pragma mark - UINavigationControllerDelegate

- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    
}

- (void)navigationController:(UINavigationController *)navigationController didShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    
}

- (nullable id <UIViewControllerInteractiveTransitioning>)navigationController:(UINavigationController *)navigationController
                                   interactionControllerForAnimationController:(id <UIViewControllerAnimatedTransitioning>)animationController {
    return self;
}

- (nullable id <UIViewControllerAnimatedTransitioning>)navigationController:(UINavigationController *)navigationController
                                            animationControllerForOperation:(UINavigationControllerOperation)operation
                                                         fromViewController:(UIViewController *)fromVC
                                                           toViewController:(UIViewController *)toVC {
        return self;
}

#pragma mark - UIViewControllerAnimationTransitioning

- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext {
    NSTimeInterval transitionDuration = 0.4;
    
    if ([self.currentInteractiveAnimation respondsToSelector:@selector(transitionDuration)]) {
        transitionDuration = [self.currentInteractiveAnimation transitionDuration];
    }
    
    return transitionDuration;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
      UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
}

#pragma mark - UIViewControllerInteractiveTransitioning

- (void)startInteractiveTransition:(id <UIViewControllerContextTransitioning>)transitionContext {
    NSLog(@"ttttttt startInteractiveTransition, current pan: %@", self.currentPanGestureRecognizer);
    
    [self updateViewsInteractionWithTransitionContext:transitionContext isEnabled:NO];

    id<LBTransitionAnimationProtocol> animation = [[LBTransitionInteractivePopLinearAnimation alloc] init];
    
    [animation prepareTransitionWithTransitionContext:transitionContext];

    if (self.currentPanGestureRecognizer == nil) {
        [animation finishTransitionIsCancel:YES completion:^(BOOL finished, BOOL isCancel, id<UIViewControllerContextTransitioning>  _Nonnull transitionContext) {
            [self finishInteractiveTransitionWithTransitionContext:transitionContext isCancel:isCancel isAnimationFinished:finished];
            
            [self updateViewsInteractionWithTransitionContext:transitionContext isEnabled:YES];
        }];
    } else {
        self.currentInteractiveAnimation = animation;
    }
}

#pragma mark - action

- (void)handlePan:(id)sender {
    if (![sender isKindOfClass:[UIPanGestureRecognizer class]]) return;
    
    UIPanGestureRecognizer *panGestureRecognizer = sender;
    CGPoint velocity = [panGestureRecognizer velocityInView:panGestureRecognizer.view.superview];
    CGPoint translation = [panGestureRecognizer translationInView:panGestureRecognizer.view.superview];
    CGFloat percentComplete = [self percentCompleteWithTranslation:translation];
    UIResponder *nextResponder = panGestureRecognizer.view.nextResponder;
    switch (panGestureRecognizer.state) {
        case UIGestureRecognizerStateBegan: {
            NSLog(@"ttttttt pan pop state began");
            
            CGFloat velocityX = velocity.x;
            CGFloat velocityY = velocity.y;
            
            if (velocityX == 0.0) {
                panGestureRecognizer.enabled = NO;
                panGestureRecognizer.enabled = YES;
                return;
            }
            self.currentPanGestureRecognizer = panGestureRecognizer;
            UINavigationController *navigationController = self.navigationController;
            id<UINavigationControllerDelegate> navigationControllerDelegate = navigationController.delegate;
            navigationController.delegate = self;
            NSLog(@"LIVDetailTransitionManager navigationControllerDelegate: %@", navigationController.delegate);
            [navigationController popViewControllerAnimated:YES];
            navigationController.delegate = navigationControllerDelegate;
        }
            break;
        case UIGestureRecognizerStateChanged: {
            NSLog(@"这里的animation %@", self.currentInteractiveAnimation);
            BOOL finishInstantly = [self.currentInteractiveAnimation updateTransitionWithPercentComplete:percentComplete translation:translation];
            
            if (finishInstantly) {
                self.currentPanGestureRecognizer.state = UIGestureRecognizerStateEnded;
            }
        }
            break;
        
        case UIGestureRecognizerStateCancelled:
              case UIGestureRecognizerStateFailed: {
                  NSLog(@"ttttttt pan pop state cancelled, failed, started: %@", self.currentInteractiveAnimation);

                  self.currentPanGestureRecognizer = nil;

                  [self.currentInteractiveAnimation finishTransitionIsCancel:YES completion:^(BOOL finished, BOOL isCancel, id<UIViewControllerContextTransitioning> transitionContext) {
                      [self finishInteractiveTransitionWithTransitionContext:transitionContext isCancel:isCancel isAnimationFinished:finished];
                  }];
              } break;
            
        case UIGestureRecognizerStateEnded: {
            BOOL isTransitionMadeProgress = percentComplete > 0.1;
            BOOL isPanRight = velocity.x > 0.0;
            BOOL shouldComplete = NO;
            shouldComplete = (isTransitionMadeProgress && isPanRight) || velocity.x > 100.0;

            NSLog(@"ttttttt pan pop state end: %d, %d, %d, %@", isTransitionMadeProgress, isPanRight, shouldComplete, self.currentInteractiveAnimation);
            self.currentPanGestureRecognizer = nil;
            __weak typeof(self) weakSelf = self;
            [self.currentInteractiveAnimation finishTransitionIsCancel:!shouldComplete completion:^(BOOL finished, BOOL isCancel, id<UIViewControllerContextTransitioning> transitionContext) {
                [self finishInteractiveTransitionWithTransitionContext:transitionContext isCancel:isCancel isAnimationFinished:finished];
                
                [self updateViewsInteractionWithTransitionContext:transitionContext isEnabled:YES];
            }];
        } break;
        default:
            break;
    }
}

- (CGFloat)percentCompleteWithTranslation:(CGPoint)translation {
    CGFloat translationX = translation.x;
    CGFloat translationXRangeMin = 0.0;
    CGFloat translationXRangeMax = 200.0;
    
    if (translationX < translationXRangeMin) {
        return 0.0;
    } else if (translationX > translationXRangeMax) {
        return 1.0;
    } else {
        return (translationX - translationXRangeMin) / (translationXRangeMax - translationXRangeMin);
    }
}

- (void)finishInteractiveTransitionWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext isCancel:(BOOL)isCancel isAnimationFinished:(BOOL)isAnimationFinished {
    NSLog(@"ttttttt finish interactive transition: %d, %d", isCancel, isAnimationFinished);
        
    id<LBTransitionAnimationProtocol> currentInteractiveAnimation = self.currentInteractiveAnimation;
    self.currentInteractiveAnimation = nil;

    if (isCancel) {
        [transitionContext cancelInteractiveTransition];
    } else {

        [transitionContext finishInteractiveTransition];
    }
    
    [transitionContext completeTransition:!isCancel];
}

- (void)updateViewsInteractionWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext isEnabled:(BOOL)isEnabled {
    UIViewController *fromViewController = [transitionContext viewControllerForKey:UITransitionContextFromViewControllerKey];
    UIViewController *toViewController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];

    fromViewController.view.userInteractionEnabled = isEnabled;
    toViewController.view.userInteractionEnabled = isEnabled;
}

@end

//
//  LBTransitionInteractivePopAnimation.h
//  TEXT
//
//  Created by mac on 2025/3/30.

//

#import <Foundation/Foundation.h>
#import "LBTransitionAnimationProtocol.h"

NS_ASSUME_NONNULL_BEGIN

@interface LBTransitionInteractivePopLinearAnimation : NSObject <LBTransitionAnimationProtocol>

@end

NS_ASSUME_NONNULL_END

//
//  LBTransitionInteractivePopAnimation.m
//  TEXT
//
//  Created by mac on 2025/3/30.
//

#import "LBTransitionInteractivePopLinearAnimation.h"
#import <UIKit/UIKit.h>
#import "UIView+LBFrame.h"

@interface LBTransitionInteractivePopLinearAnimation ()

@property (strong, nonatomic) id<UIViewControllerContextTransitioning> transitionContext;

@property (strong, nonatomic) UIView *containerView;

@property (strong, nonatomic) UIView *animationContainerView;

@end

@implementation LBTransitionInteractivePopLinearAnimation

- (instancetype)init{
    self = [super init];
    if (self == nil) return nil;
    
    return self;
}

#pragma mark - Transition

- (NSTimeInterval)transitionDuration {
    return 0.4;
}

- (void)prepareTransitionWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext {
    self.transitionContext = transitionContext;

    UIView *containerView = transitionContext.containerView;
    containerView.backgroundColor = UIColor.clearColor;

    self.containerView = containerView;

    UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey];
    toView.frame = containerView.bounds;
    toView.x = -(containerView.width / 4.0);
    
    [self.containerView addSubview:toView];
    
    UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    fromView.frame = containerView.bounds;
    
    [self.containerView addSubview:fromView];
}

- (BOOL)updateTransitionWithPercentComplete:(CGFloat)percentComplete translation:(CGPoint)translation {
    UIView *toView = [self.transitionContext viewForKey:UITransitionContextToViewKey];
    UIView *fromView = [self.transitionContext viewForKey:UITransitionContextFromViewKey];
    UIView *containerView = self.transitionContext.containerView;

    CGFloat toViewInitialX = containerView.x - (containerView.width / 4.0);
    CGFloat toViewMaxX = containerView.x;
    CGFloat fromViewInitialX = containerView.x;
    CGFloat fromViewMaxX = containerView.width;
    
    toView.x = containerView.x - (containerView.width / 4.0) + translation.x * 0.25;
    fromView.x = containerView.x + translation.x;
    
    if (toView.x < toViewInitialX) {
        toView.x = toViewInitialX;
    }
    
    if (toView.x > toViewMaxX) {
        toView.x = toViewMaxX;
    }
    
    if (fromView.x < fromViewInitialX) {
        fromView.x = fromViewInitialX;
    }
    
    if (fromView.x > fromViewMaxX) {
        fromView.x = fromViewMaxX;
    }
    
    [self.transitionContext updateInteractiveTransition:percentComplete];
    
    return NO;
}

- (void)finishTransitionIsCancel:(BOOL)isCancel completion:(LIVDetailTransitionAnimationCompletion)completion {
    UIView *toView = [self.transitionContext viewForKey:UITransitionContextToViewKey];
    UIView *fromView = [self.transitionContext viewForKey:UITransitionContextFromViewKey];
    UIView *containerView = self.transitionContext.containerView;

    CGFloat toViewInitialX = containerView.x - (containerView.width / 4.0);
    CGFloat toViewMaxX = containerView.x;
    CGFloat fromViewInitialX = containerView.x;
    CGFloat fromViewMaxX = containerView.width;

    [UIView animateWithDuration:[self transitionDuration] delay:0.0 usingSpringWithDamping:1.0 initialSpringVelocity:0.0 options:UIViewAnimationOptionLayoutSubviews | UIViewAnimationOptionCurveLinear animations:^{
        if (isCancel) {
            toView.x = toViewInitialX;
            fromView.x = fromViewInitialX;
        } else {
            toView.x = toViewMaxX;
            fromView.x = fromViewMaxX;
        }
    } completion:^(BOOL finished) {
        NSLog(@"interactive pop linear animation completion: %d, %d", finished, isCancel);

        if (completion != nil) {
            completion(finished, isCancel, self.transitionContext);
        }
    }];
}

@end

//
//  LBTransitionAnimationProtocol.h
//  TEXT
//
//  Created by mac on 2025/3/30.
//

#ifndef LBTransitionAnimationProtocol_h
#define LBTransitionAnimationProtocol_h
#import <UIKit/UIKit.h>

typedef void(^LIVDetailTransitionAnimationCompletion)(BOOL finished, BOOL isCancel, id<UIViewControllerContextTransitioning> transitionContext);

@protocol LBTransitionAnimationProtocol <NSObject>

- (void)prepareTransitionWithTransitionContext:(id<UIViewControllerContextTransitioning>)transitionContext;
- (BOOL)updateTransitionWithPercentComplete:(CGFloat)percentComplete translation:(CGPoint)translation; // 更新当前动画进度,返回 YES 代表需要马上结束转场
- (void)finishTransitionIsCancel:(BOOL)isCancel completion:(LIVDetailTransitionAnimationCompletion)completion;

- (NSTimeInterval)transitionDuration;

@end


#endif /* LBTransitionAnimationProtocol_h */