iOS 12兼容:Swift中替代sheetPresentationController实现底部模态弹窗
兼容iOS12的底部模态实现方案
要解决iOS12及以下版本无法使用sheetPresentationController实现底部模态的问题,需要分版本做适配处理:iOS15+用系统原生能力,iOS12-14自定义实现底部弹出效果。
一、版本分支处理
先通过系统版本宏做条件编译,避免低版本编译报错:
if (@available(iOS 15.0, *)) { // iOS15及以上用系统原生Sheet UISheetPresentationController *sheet = self.bottomController.sheetPresentationController; sheet.detents = @[[UISheetPresentationControllerDetent mediumDetent]]; sheet.prefersGrabberVisible = YES; // 可选,显示顶部抓手 [self presentViewController:self.bottomController animated:YES completion:nil]; } else { // iOS12-14自定义底部模态 [self presentCustomBottomModal:self.bottomController]; }
二、iOS12-14自定义底部模态实现
这里提供两种简单可行的实现方式:
方式1:自定义转场动画
- 实现自定义转场代理类,遵循
UIViewControllerTransitioningDelegate和UIViewControllerAnimatedTransitioning协议:
@interface BottomModalTransitionDelegate : NSObject <UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning> @property (nonatomic, assign) BOOL isPresenting; @end @implementation BottomModalTransitionDelegate - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source { self.isPresenting = YES; return self; } - (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed { self.isPresenting = NO; return self; } - (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext { return 0.35; } - (void)animateTransition:(id <UIViewControllerContextTransitioning>)transitionContext { UIViewController *toVC = transitionContext.viewControllerForKey:UITransitionContextToViewControllerKey]; UIViewController *fromVC = transitionContext.viewControllerForKey:UITransitionContextFromViewControllerKey]; if (self.isPresenting) { // 弹出动画:从底部滑入 CGRect finalFrame = transitionContext.finalFrameForViewController(toVC); toVC.view.frame = CGRectMake(0, transitionContext.containerView.bounds.size.height, finalFrame.size.width, finalFrame.size.height); [transitionContext.containerView addSubview:toVC.view]; // 添加半透明背景遮罩 UIView *bgMask = [[UIView alloc] initWithFrame:transitionContext.containerView.bounds]; bgMask.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.5]; bgMask.tag = 1001; [transitionContext.containerView insertSubview:bgMask belowSubview:toVC.view]; [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ toVC.view.frame = finalFrame; } completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }]; } else { // 消失动画:滑回底部 [UIView animateWithDuration:[self transitionDuration:transitionContext] animations:^{ fromVC.view.frame = CGRectMake(0, transitionContext.containerView.bounds.size.height, fromVC.view.frame.size.width, fromVC.view.frame.size.height); UIView *bgMask = [transitionContext.containerView viewWithTag:1001]; bgMask.alpha = 0; } completion:^(BOOL finished) { [transitionContext completeTransition:YES]; }]; } } @end
- 在
presentCustomBottomModal:方法中配置控制器:
- (void)presentCustomBottomModal:(UIViewController *)modalVC { // 设置模态样式为自定义 modalVC.modalPresentationStyle = UIModalPresentationCustom; BottomModalTransitionDelegate *transitionDelegate = [[BottomModalTransitionDelegate alloc] init]; modalVC.transitioningDelegate = transitionDelegate; // 设置控制器的 preferredContentSize,控制弹出高度(类似detent的效果) modalVC.preferredContentSize = CGSizeMake([UIScreen mainScreen].bounds.size.width, 400); // 自定义高度 [self presentViewController:modalVC animated:YES completion:nil]; }
方式2:直接添加视图到当前控制器(更轻量)
如果不需要完整的模态生命周期,也可以直接把模态控制器的view添加到当前控制器的view层级:
- (void)showBottomModalView:(UIViewController *)modalVC { // 添加背景遮罩 UIView *bgMask = [[UIView alloc] initWithFrame:self.view.bounds]; bgMask.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0]; bgMask.tag = 1002; [self.view addSubview:bgMask]; // 配置模态视图的初始位置 CGFloat modalHeight = 400; modalVC.view.frame = CGRectMake(0, self.view.bounds.size.height, self.view.bounds.size.width, modalHeight); [self.view addSubview:modalVC.view]; [self addChildViewController:modalVC]; [modalVC didMoveToParentViewController:self]; // 添加点击遮罩关闭的手势 UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(hideBottomModal)]; [bgMask addGestureRecognizer:tap]; // 执行弹出动画 [UIView animateWithDuration:0.35 animations:^{ bgMask.alpha = 0.5; modalVC.view.frame = CGRectMake(0, self.view.bounds.size.height - modalHeight, self.view.bounds.size.width, modalHeight); }]; } - (void)hideBottomModal { UIViewController *modalVC = [self.childViewControllers lastObject]; // 根据实际情况调整获取方式 UIView *bgMask = [self.view viewWithTag:1002]; [UIView animateWithDuration:0.35 animations:^{ bgMask.alpha = 0; modalVC.view.frame = CGRectMake(0, self.view.bounds.size.height, modalVC.view.frame.size.width, modalVC.view.frame.size.height); } completion:^(BOOL finished) { [modalVC willMoveToParentViewController:nil]; [modalVC.view removeFromSuperview]; [modalVC removeFromParentViewController]; [bgMask removeFromSuperview]; }]; }
三、注意事项
- 自定义实现时要处理屏幕旋转的适配,确保模态视图的frame在旋转后正确更新
- 如果是UINavigationController作为模态内容,要确保其导航栏样式和布局符合需求
- 可以根据需要调整动画时长、背景透明度、模态高度等参数,匹配设计效果
内容的提问来源于stack exchange,提问作者Xavier Ojeda Aguilar
相关产品推荐
相关产品推荐

