首页 > 技术文章 > UIPresentationController - iOS自定义模态弹出框

huangzhengguo 2018-12-05 18:29 原文

参考:

https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/DefiningCustomPresentations.html

首先说下需求,就是一个自定义的模态弹出框,这种需求应该很广

对于弹出框,我们首先想到的就是UIAlertController这个类。但是这个类只能创建两种类型的弹出框,actionSheet和alert。要想使用这个类实现上面的效果,很难,之前为了实现这个效果在网上找来找去,最后使用了View叠加View的方式,也就是一个大的View当做背景,中间的控件使用一个View来实现。这种方法有个缺点,就是当显示后,导航栏的按钮,比如返回按钮还是可以点击的,也就是整个View并不能覆盖真个屏幕,实现真正的模态。而且使用View看起来效果也不好。

那么下面就来说说怎么实现上面图中的效果,真正的模态弹出框。

要实现上面的效果,首先我们需要一个ViewController来实现要弹出的框,也就是上图中的时间选择部分,那么谁来控制这个ViewController的显示呢?我们就需要下面一个类

有一个类是必不可少的:UIPresentationController,这个类看起来很陌生。我们知道从一个ViewController跳转到另一个,有两种方式:

1)经常使用的是push的方式,控制器从屏幕的右边弹出来,显示的控制器带有返回按钮

2) 还有一种方式就是present,这种方式可能不经常使用,默认的效果是从屏幕底部弹出来的,并占满整个屏幕,没有返回按钮

上面的时间选择弹出框就是通过第二种方式实现的,在我们调用present的时候,系统会提供一个默认的UIPresentationController,但是UIKit默认的实现不能满足我们的需求,但是这个类是可以自定义的,用来控制present的方式。

那个这个UIPresentationController可以用来干什么呢?有以下四个功能:

1)  设置弹出框的大小,上图中也就是中间时间选择框的大小

2) 添加视图来改变展示内容的外观

3) 支持自定义视图的转场动画

4) 适配展示的外观当App环境改变时,比如屏幕旋转

那么UIKit怎么才会使用我们自定义的UIPresentationController呢?需要以下两个条件:

1)当你present一个控制器的时候,设置其modalPresentationStyle为custom,也就是指定present的类型为自定义的,不使用系统的

2)添加一个UIViewControllerTransitionDelegate代理,这个代理用来获取自定义的UIPresentationController

 

实现模态框的处理流程:

1)当present的时候,UIKit将会调用transitioning代理方法

func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController?

来获取我们自定义的UIPresentationController

2) 请求 transitioning 代理获取动画和交互动画对象,如果存在

3) 调用自定义 UIPresentationController 的 presentationTransitionWillBegin 方法,这个方法的实现应该是添加自定义的视图到视图层级中和为这些视图配置动画

4) 从自定义的 UIPresentationController 中获取 presentedView

  这个视图被动画对象操作并安放到指定的位置。通常 presentedView 返回的是要 present 的视图控制器的根视图。如果有需要,我们的 presentationController 可以使用一个背景视图替换到这个根视图。

如果你指定一个不同的视图,你必须把 presented 的视图控制器的根视图嵌入到视图层级中。

5) 执行转场动画

  转场动画包含主要的一个,由动画对象创建。还有任意你配置的动画,伴随主要动画。

在动画的处理过程中,UIKit将会调用 containerViewWillLayoutSubviews 和 containerViewDidLayoutSubviews,在这两个方法中,你可以根据你的需要调整自定义视图。

6) 最后当转场动画结束的时候调用 presentationTranistionDidEnd

 

以上就是 present 过程中所做的处理

 

模态框 dismiss 的处理流程

1) 从当前可见的视图控制器获取自定义的UIPresentationController

2) 请求 transitioning 代理获取动画和交互动画对象,如果存在

3) 调用 presentationController 的 dismissalTransitionWillBegin

  这个方法的实现可以添加自定义视图及动画。

4) 获取已经存在的 presentedView

5) 执行转场动画

  转场动画包含了主要的一个,由动画对象创建。还有任意你配置的动画,伴随主要动画。

在动画处理的过程中,UIKit调用 containerViewWillLayoutSubviews 和 containerViewDidLayoutSubviews,在这两个方法中,你可以删除任何自定义的约束。

6) 最后当转场动画结束时调用 dismissalTransitionDidEnd

 

注意:在 present 的过程中 frameOfPresentedViewInContainerView 和 presentedView 将会调用数次,所以这两个方法的实现必须要简单,快速返回。而且,presentedView 的实现不应该设置视图层级,在调用 presentedView 的时候,视图层级应该已经设置好了。

 

下面就来看看怎么自定义UIPresentationController?怎么使用?

 

创建一个自定义的Presentation Controller

 

 为了实现自定义的 presentation style,你需要从 UIPresentationController 继承编写代码实现自定义的视图和动画效果。

 设置被 presented 的视图控制器的 frame

  你可以修改 presented 控制器的 frame 来实现只占用可用空间的部分空间。presented 控制器默认占用整个 container 视图。为了改变这个 frame 占用的空间,可以重写 presentation 控制器的 frameOfPresentedViewInContainerView。代码可以如下:

    override var frameOfPresentedViewInContainerView: CGRect {
        let containerBounds = self.containerView?.bounds
        let presentedViewFrame = CGRect.init(x: (containerBounds?.size.width)! / 2.0 - 150, y: (containerBounds?.size.height)! / 2 - 200, width: 300.0, height: 330.0)
        
        return presentedViewFrame
    }

管理和呈现自定义视图的动画

 自定义的 presentation 经常涉及添加自定义的视图到要 presented 的内容中。使用自定义的视图可以显示纯粹的视觉上的装饰或者添加实际上行为到 presentation 上。比如你想在 presented 内容的外部添加手势。

自定义的 presentation controller 负责所有和 presentation 关联的自定义的视图的创建和管理。一般创建视图的操作在初始化方法中编写:

    var dimmingView: UIView?
    
    override init(presentedViewController: UIViewController, presenting presentingViewController: UIViewController?) {
        super.init(presentedViewController: presentedViewController, presenting: presentingViewController)
        
        self.dimmingView = UIView.init()
        self.dimmingView?.backgroundColor = UIColor.white.withAlphaComponent(0.5)
        self.dimmingView?.alpha = 0.0
    }

 

你给你的视图在显示到屏幕上的时候添加动画,在方法 presentationTransitionWillBegin 中实现。如下:

    override func presentationTransitionWillBegin() {
        let containerView = self.containerView
        // 设置背景透明度
        // containerView?.backgroundColor = UIColor.lightGray.withAlphaComponent(0.6)
        
        let presentedViewController = self.presentedViewController
        
        self.dimmingView?.frame = (containerView?.bounds)!
        self.dimmingView?.alpha = 0.0
        
        containerView?.insertSubview(self.dimmingView!, at: 0)
        
        if (presentedViewController.transitionCoordinator != nil) {
            presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
                self.dimmingView?.alpha = 1.0
            }, completion: nil)
        } else {
            self.dimmingView?.alpha = 1.0
        }
    }

 

在 presentation 的最后阶段,我们可以在 presentationTransitionDidEnd 中实现由于 presentation 被取消引起的清理工作。一个交互式的动画对象可能取消转场如果它的临界条件不符合的话。当这种情况发生的时候,UIkit 将会调用 presentationTransitionDidEnd。当一个取消发生的时候,我们需要移除之前添加的视图和恢复任何其它视图的配置。

    override func presentationTransitionDidEnd(_ completed: Bool) {
        if !completed {
            self.dimmingView!.removeFromSuperview()
        }
    }

 

当我们 dismiss 视图控制器的时候会调用 dismissalTransitionWillBegin 以及 dismissalTransitionDidEnd,如果你想要使用动画让你的视图消失,可以在 dismissalTransitionWillBegin 配置,以及在 dismissalTransitionDidEnd 移除自定义的视图,如下:

    override func dismissalTransitionWillBegin() {
        if self.presentedViewController.transitionCoordinator != nil {
            self.presentedViewController.transitionCoordinator?.animate(alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) in
                self.dimmingView?.alpha = 0.0
            }, completion: nil)
        } else {
            self.dimmingView?.alpha = 0.0
        }
    }
    
    override func dismissalTransitionDidEnd(_ completed: Bool) {
        if completed {
            self.dimmingView?.removeFromSuperview()
        }
    }

 

至此,整个过程都实现了,如果是不需要背景半透明状态,可以不用添加自定义的视图。里面有一些地方还是不很理解,由于很少使用动画,所以没有实现更加高级的动画操作,里面的 transitionCoordinator 也没做研究,以后用到再说。总的来说一个模态弹出框已经可以使用了。还请多多指教。

实例地址:https://github.com/huangzhengguo/iOSSamples/tree/master/SwiftTools/SwiftTools/Tools/ModalController

推荐阅读