封装了一个iOS评论弹窗

封装了一个iOS类似抖音效果的评论弹窗,可以跟手滑动的效果
主要有下面两需要注意的点

双手势响应

因为我们的弹窗既要支持拖动整体上下滑动,还要支持内容列表的滑动
,所以,我们需要在内容视图中添加一个滑动的手势,以此来滑动弹窗
并且要支持同时响应,同时要注意,支持同时响应并不是同时滑动,
而是支持手指在列表中的时候,可以响应弹窗的手势

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    NSLog(@"shouldRecognizeSimultaneouslyWithGestureRecognizer: %@", gestureRecognizer);
    if (gestureRecognizer == self.panGesture) {
        if ([otherGestureRecognizer isEqual:self.scrollView.panGestureRecognizer]) {
            return YES;
        }
    }
    return NO;
}

滑动弹窗的时候,禁止列表滚动

这个很好理解,因为如果我们滑动弹窗的时候,仍然滑动列表,那样的用户体验就会很差,所以我们要处理的就是在我们滚动弹窗的时候,禁止列表的滚动

代码

.h

//
//  LBSlidePopView.h
//  TEXT
//
//  Created by mac on 2024/7/28.
//  Copyright © 2024 刘博. All rights reserved.
//

#import 

NS_ASSUME_NONNULL_BEGIN

@protocol LBSlidePopViewDelegate 

- (void)slideDismiss:(NSString *)source;

@optional

- (void)didPanGesture:(UIPanGestureRecognizer *)panGesture;

@end

typedef NS_ENUM(NSInteger, LBSlideViewExpand) {
    LBSlideViewExpandNone = 0,   // 无
    LBSlideViewExpandMin = 1,
    LBSlideViewExpandMid = 2,
    LBSlideViewExpandMax = 3
};

@protocol LBPopSlideFrameDelegate 

- (void)frameChanged:(CGRect)frame percent:(CGFloat)percent isMax:(BOOL)isMax inAnimation:(BOOL)inAnimation finished:(BOOL)finished;
- (void)frameChangedInAnimationBlock:(CGRect)frame percent:(CGFloat)percent isMax:(BOOL)isMax;
- (void)expandStatusChanged:(LBSlideViewExpand)status lastStatus:(LBSlideViewExpand)lastStatus;

// Offer区块
- (BOOL)shouldCollopseOfferView;
- (BOOL)shouldExpandOfferView;
- (void)updateOfferViewFrame:(CGFloat)offset;
- (void)collopseOrExpandOfferView;

@end

@interface LBSlidePopView : UIView

@property (nonatomic, weak) id frameDelegate;

@property (nonatomic, weak) UIView                     *contentView;   // 主内容

@property (nonatomic, strong) UITapGestureRecognizer    *tapGesture;
@property (nonatomic, strong) UIPanGestureRecognizer    *panGesture;
@property (nonatomic, strong) UILongPressGestureRecognizer *longPressGesture;

@property (nonatomic, weak) UIScrollView                *scrollView;
@property (nonatomic, assign) CGFloat beginContentOffsetY;
@property (nonatomic, assign) BOOL                      isDragScrollView;
@property (nonatomic, assign) CGFloat                   lastTransitionY;
@property (nonatomic, assign) CGFloat                   beginY;
@property (nonatomic, assign) BOOL                      hasImpactFeedback;   // 是否已经震动过
@property (nonatomic, assign) CGFloat                    slideHPercent;   // 横向滚动的阈值:用来判断是否横向滚动还是竖向滚动
///允许横向滑动消失,默认NO
@property (nonatomic, assign) BOOL horizontalPanDismiss;

@property (nonatomic, weak) id delegate;

@property (nonatomic, assign) BOOL                    disableGesture;   // 是否禁用手势

//原来的内容的高度
@property (nonatomic, assign) CGFloat contentOriginHeight;


- (instancetype)initWithFrame:(CGRect)frame contentView:(UIView *)contentView maskView:(nullable UIView *)maskView delegate:(id)delegate;

- (void)updateContentFrame:(CGFloat)offset;
- (void)dismiss:(NSString *)source;

- (void)updateForSingleView;

@end

NS_ASSUME_NONNULL_END

.m

//
//  LBSlidePopView.m
//  TEXT
//
//  Created by mac on 2024/7/28.
//

#import "LBSlidePopView.h"
#import "LBFunctionTestHeader.h"

@interface LBSlidePopView ()

@property (nonatomic, weak) UIView *maskView;

@end

@implementation LBSlidePopView

- (instancetype)initWithFrame:(CGRect)frame contentView:(UIView *)contentView
                     maskView:(UIView *)maskView delegate:(id)delegate {
    if (self = [super initWithFrame:frame]) {
        self.backgroundColor = [UIColor clearColor];
        self.delegate = delegate;
        self.maskView = maskView;
        self.contentView = contentView;
        [self addSubview:self.maskView];
        [self addSubview:self.contentView];
        
        // 横滑幅度较大,幅度可开关控制,默认是60°夹角
        self.slideHPercent = 0.58;
        
        // 添加手势
        [self addGestureRecognizer:self.tapGesture];
        [self addGestureRecognizer:self.panGesture];
    }
    return self;
}

- (void)updateForSingleView {
    self.zf_height = self.contentView.zf_height;
    self.contentView.zf_top = 0;
    [self.maskView removeFromSuperview];
}

- (void)dealloc {
    NSLog(@"##** LIVSlidePopupView dealloc");
}

- (void)setDisableGesture:(BOOL)disableGesture {
    if (_disableGesture != disableGesture) {
        _disableGesture = disableGesture;
        if (disableGesture) {
            [self removeGestureRecognizer:self.tapGesture];
            [self removeGestureRecognizer:self.panGesture];
        } else {
            [self addGestureRecognizer:self.tapGesture];
            [self addGestureRecognizer:self.panGesture];
        }
    }
}

- (void)showWithCompletion:(void (^)(void))completion {
    [UIView animateWithDuration:0.25f animations:^{
        self.contentView.zf_bottom = self.zf_height;
    } completion:^(BOOL finished) {
        !completion ? : completion();
        [self frameChanged:NO];
    }];
}

- (void)dismiss:(NSString *)source {
    NSLog(@"");
    if (self.delegate && [self.delegate respondsToSelector:@selector(slideDismiss:)]) {
        [self.delegate slideDismiss:source];
    }
    
    [self frameChanged:YES];
}

#pragma mark - UIGestureRecognizerDelegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    NSLog(@"gestureRecognizer shouldReceiveTouch: %@", gestureRecognizer);
    if (gestureRecognizer == self.panGesture) {
        UIView *touchView = touch.view;
        while (touchView != nil) {
            NSLog(@"%@", touchView);
            if ([touchView isKindOfClass:NSClassFromString(@"EmotionBoardScrollView")]
                || [touchView isKindOfClass:[UITextView class]]) {
                // 滑动Emoji键盘、输入框时不处理
                self.isDragScrollView = NO;
                return NO;
            }
            if (touchView == self.scrollView) {
                self.isDragScrollView = YES;
                break;
            } else if (touchView == self.contentView) {
                self.isDragScrollView = NO;
                break;
            }
            touchView = (UIView *)[touchView nextResponder];
        }
    }
    return YES;
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
    NSLog(@"gestureRecognizerShouldBegin: %@", gestureRecognizer);
    if (gestureRecognizer == self.tapGesture) {
        CGPoint point = [gestureRecognizer locationInView:self.contentView];
        if ([self.contentView.layer containsPoint:point] && gestureRecognizer.view == self) {
            return NO;
        }
    } else if (gestureRecognizer == self.panGesture) {
        CGPoint translation = [self.panGesture translationInView:self.contentView];
        if (!self.horizontalPanDismiss) {
            // 不处理禁用横滑的手势
            if (ABS(translation.y) == 0) {   // 完全横滑
                return NO;
            } else if ((ABS(translation.x) / ABS(translation.y)) > self.slideHPercent) {   // 横滑幅度较大,幅度可开关控制,默认是60°夹角
                return NO;
            }
        }
        self.beginY = self.zf_top;
    }
    self.hasImpactFeedback = NO;
    return YES;
}

//是否支持多手势触发,返回YES,则可以多个手势一起触发方法,返回NO则为互斥
//是否允许多个手势识别器共同识别,一个控件的手势识别后是否阻断手势识别继续向下传播,默认返回NO;
//如果为YES,响应者链上层对象触发手势识别后,如果下层对象也添加了手势并成功识别也会继续执行,否则上层对象识别后则不再继续传播
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
    NSLog(@"shouldRecognizeSimultaneouslyWithGestureRecognizer: %@", gestureRecognizer);
    if (gestureRecognizer == self.panGesture) {
        if ([otherGestureRecognizer isEqual:self.scrollView.panGestureRecognizer]) {
            return YES;
        }
    }
    return NO;
}

 这个方法返回YES,第一个手势和第二个互斥时,第一个会失效
//- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
//    return NO;
//}
//
这个方法返回YES,第一个和第二个互斥时,第二个会失效
//- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer {
//    return NO;
//}

#pragma mark - HandleGesture

- (void)handleTapGesture:(UITapGestureRecognizer *)tapGesture {
    CGPoint point = [tapGesture locationInView:self.contentView];
    if (![self.contentView.layer containsPoint:point] && tapGesture.view == self) {
        [self dismiss:@"blankArea"];
    }
}

- (void)handlePanGesture:(UIPanGestureRecognizer *)panGesture {
    CGPoint translationPoint = [panGesture translationInView:self.contentView];
    CGFloat translationOffset = translationPoint.y;
    BOOL isHorizontal = NO;
    if ((ABS(translationPoint.y) == 0 || (ABS(translationPoint.x) / ABS(translationPoint.y)) > self.slideHPercent)
        && self.horizontalPanDismiss) {
        translationOffset = translationPoint.x;
        // 根据contentOffset变化判断scrollView是否有滚动过,scrollView没有滚动过才能支持横向滑动关闭面板
        isHorizontal = ABS(self.beginContentOffsetY - self.scrollView.contentOffset.y) < 0.5;
    }
    if (self.isDragScrollView) {
        // 当UIScrollView在最顶部时,处理视图的滑动
        if (self.scrollView.contentOffset.y <= 0 || isHorizontal) {
            if (translationOffset > 0) { // 向右、向下拖拽
//                self.scrollView.contentOffset = CGPointZero;
                self.scrollView.panGestureRecognizer.enabled = NO;
                self.isDragScrollView = NO;
                
                [self updateContentFrame:translationOffset];
            }
        }
    } else {
        [self updateContentFrame:translationOffset];
    }
    
    [panGesture setTranslation:CGPointZero inView:self.contentView];
    if (panGesture.state == UIGestureRecognizerStateBegan) {
        // 手势开始时记录contentOffset初始位置
        self.beginContentOffsetY = self.scrollView.contentOffset.y;
    }
    else if (panGesture.state == UIGestureRecognizerStateEnded) {
        CGPoint velocityPoint = [panGesture velocityInView:self.contentView];
        
        self.scrollView.panGestureRecognizer.enabled = YES;
        
        // (结束时的速度>0 滑动距离> 5) 或 绝对移动距离超过150 且UIScrollView滑动到最顶部
        CGFloat velocityOffset = velocityPoint.y;
        if ((ABS(velocityPoint.y) == 0 || (ABS(velocityPoint.x) / ABS(velocityPoint.y)) > self.slideHPercent)
             && self.horizontalPanDismiss) {
            velocityOffset = velocityPoint.x;
        }
        if (((velocityOffset > 0 && self.lastTransitionY > 5) || (self.contentView.zf_bottom - self.zf_height) > 150)  && !self.isDragScrollView) {
            [self dismiss:@"swipeDown"];
        } else {
            [self showWithCompletion:nil];
        }
    }
    
    self.lastTransitionY = translationOffset;
    
    if (self.delegate && [self.delegate respondsToSelector:@selector(didPanGesture:)]) {
        [self.delegate didPanGesture:panGesture];
    }
}

// 更新内容区域的frame
- (void)updateContentFrame:(CGFloat)offset {
    CGFloat topY = (self.zf_height - self.contentView.zf_height);
    self.contentView.zf_top = MAX(topY, self.contentView.zf_top + offset);
    
    // 下拉时蒙层透明度变化
    CGFloat rate = 1.f;
    if (self.contentView.zf_height > 0 && self.contentView.zf_bottom > self.zf_height) {
        rate = MIN(1.f, (self.zf_height - self.contentView.zf_top) / self.contentView.zf_height);
        rate = MAX(0.f, rate);
    }
    self.maskView.alpha = 0.55 * rate;
    
    [self frameChanged:NO];
}

- (void)frameChanged:(BOOL)dismiss {
    if (self.frameDelegate && [self.frameDelegate respondsToSelector:@selector(frameChanged:percent:isMax:inAnimation:finished:)]) {
        CGRect frame = CGRectZero;
        if (!dismiss) {
            frame.size.height = self.contentOriginHeight - self.contentView.zf_top;
        }
        [self.frameDelegate frameChanged:frame percent:!dismiss isMax:!dismiss inAnimation:YES finished:dismiss];
    }
}

#pragma mark - 懒加载

- (UITapGestureRecognizer *)tapGesture {
    if (!_tapGesture) {
        _tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
        _tapGesture.delegate = self;
    }
    return _tapGesture;
}

- (UIPanGestureRecognizer *)panGesture {
    if (!_panGesture) {
        _panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
        _panGesture.delegate = self;
    }
    return _panGesture;
}

@end

弹窗中的代码

//
//  LBSlidePopViewController.m
//  TEXT
//
//  Created by mac on 2024/8/18.
//  Copyright © 2024 刘博. All rights reserved.
//

#import "LBSlidePopViewController.h"
#import "LBSlidePopView.h"
#import "LBFunctionTestHeader.h"
#import "LBview.h"

@interface LBSlidePopViewController () 

@property (nonatomic, strong) UITableView *tableView;
@property (nonatomic, strong) LBSlidePopView *slideView;

@end

@implementation LBSlidePopViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self setUpUI];
    [self.tableView reloadData];
    // Do any additional setup after loading the view.
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    [self.navigationController setNavigationBarHidden:true animated:animated];
}

- (void)setUpUI
{
    self.view.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
    CGFloat top = SCREEN_HEIGHT - 400;
    self.viewContent = [[LBview alloc] initWithFrame:CGRectMake(0, top, SCREEN_WIDTH, 400)];
    self.viewContent.backgroundColor = [UIColor purpleColor];
    self.view.backgroundColor = [UIColor whiteColor];
    
    //半透明背景
    self.viewDismiss = [[UIView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT)];
    self.viewDismiss.alpha = 0.0;
    self.slideView = [[LBSlidePopView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT) contentView:self.viewContent maskView:self.viewDismiss delegate:self];
    self.view = self.slideView;
    [self.slideView addGestureRecognizer:self.slideView.tapGesture];
    self.view.backgroundColor = [UIColor clearColor];
    self.view.window.backgroundColor = [UIColor clearColor];
    [self.viewContent addSubview:self.tableView];
    
    self.slideView.scrollView = self.tableView;
}

#pragma mark - UITableViewDelegate, UITableViewDataSource

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 100;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 40;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"kkk"];
    return cell;
}

#pragma mark - LBSlidePopViewDelegate

- (void)slideDismiss:(NSString *)source
{
    [self dismissViewControllerAnimated:YES completion:nil];
}

#pragma mark - lazy load

- (UITableView *)tableView
{
    if (!_tableView) {
        _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, SCREEN_WIDTH, 400) style:UITableViewStylePlain];
        [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"kkk"];
        _tableView.delegate = self;
        _tableView.dataSource = self;
    }
    return _tableView;
}

/*
#pragma mark - Navigation

// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    // Get the new view controller using [segue destinationViewController].
    // Pass the selected object to the new view controller.
}
*/

@end

代码
链接: link

你可能感兴趣的:(ios,cocoa,macos)