You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:自定义转场动画

  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
  1. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 08:54:17