前言
在我们平时日常开发中,经常会遇到各种样式的弹框。你是否也经常遇到呢?你是如何实现的?
本文介绍使用UIPresentationController
,结合自定义转场动效,实现一个高度自定义的弹框,这也是苹果比较推荐的一种实现方式。
预备知识
开始之前,我们要了解下几个知识点:
UIPresentationController
UIViewControllerTransitioningDelegate
UIViewControllerAnimatedTransitioning
1、UIPresentationController
是什么?官方文档中介绍如下:
An object that manages the transition animations and the presentation of view controllers onscreen.
简单来说,它可以管理转场动画和模态出来的窗口控制器。详细信息可以参考:UIPresentationController文档
2、UIViewControllerTransitioningDelegate
定义了转场代理方法,可以指定Presented
和Dismissed
动画,以及UIPresentationController
。
3、UIViewControllerAnimatedTransitioning
就是转场动画协议,我们可以遵守该协议,实现转场动画。
实现
1、自定义UIPresentationController
,并实现相应方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| struct ZCXPopoup {}
extension ZCXPopoup {
class PresentationController: UIPresentationController {
override func presentationTransitionWillBegin() { guard let containerView else { return } dimmingView.frame = containerView.bounds dimmingView.alpha = 0.0 containerView.insertSubview(dimmingView, at: 0)
// 背景蒙层淡入动画 presentedViewController.transitionCoordinator?.animate { _ in self.dimmingView.alpha = 1.0 } }
override func dismissalTransitionWillBegin() { // 背景蒙层淡出动画,以及移除操作 presentedViewController.transitionCoordinator?.animate(alongsideTransition: { _ in self.dimmingView.alpha = 0.0 }, completion: { _ in self.dimmingView.removeFromSuperview() }) }
override var frameOfPresentedViewInContainerView: CGRect { UIScreen.main.bounds }
override func containerViewWillLayoutSubviews() {
guard let containerView else { return } dimmingView.frame = containerView.bounds
guard let presentedView else { return } presentedView.frame = frameOfPresentedViewInContainerView }
// MARK: -
/// 背景蒙层 private lazy var dimmingView: UIView = { let view = UIView() view.backgroundColor = UIColor.black.withAlphaComponent(0.5) return view }() } }
|
代码比较简单,主要的工作就是添加了一个背景蒙层,以及蒙层的动画交互处理,加上子视图尺寸的控制。
注:上面的ZCXPopoup
结构体没有实际作用,仅仅是为了区分命名空间。
2、UIViewControllerAnimatedTransitioning
实现类实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| extension ZCXPopoup {
class TransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
private var isOpen: Bool = false
convenience init(isOpen: Bool = false) { self.init() self.isOpen = isOpen }
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { transitionContext?.isAnimated == true ? 0.5 : 0 }
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let fromView = transitionContext.viewController(forKey: .from)?.view else { return } guard let toView = transitionContext.viewController(forKey: .to)?.view else { return }
if isOpen { transitionContext.containerView.addSubview(toView) toView.transform = .init(scaleX: 0.7, y: 0.7) toView.alpha = 0 }
UIView.animate( withDuration: transitionDuration(using: transitionContext), delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.7, options: []) { if self.isOpen { toView.transform = .identity toView.alpha = 1 } else { fromView.transform = .init(scaleX: 0.7, y: 0.7) fromView.alpha = 0 } } completion: { _ in let wasCancelled = transitionContext.transitionWasCancelled transitionContext.completeTransition(!wasCancelled) } } } }
|
这个实现类的内容也较简单,主要是设置转场动画时长,以及实现转场动画,转场动画分为进场(present
)和出场(dismiss
)动画。
3、UIViewControllerTransitioningDelegate
实现类实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| extension ZCXPopoup {
class TransitioningDelegate: NSObject, UIViewControllerTransitioningDelegate {
func presentationController( forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController ) -> UIPresentationController? { PresentationController(presentedViewController: presented, presenting: presenting) }
func animationController( forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController ) -> UIViewControllerAnimatedTransitioning? { TransitionAnimator(isOpen: true) }
func animationController( forDismissed dismissed: UIViewController ) -> UIViewControllerAnimatedTransitioning? { TransitionAnimator(isOpen: false) } } }
|
在该实现类中,实现代理方法,分别返回自定义的PresentationController
和TransitionAnimator
即可。
4、为控制器增加一个扩展,方便使用弹框交互
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| extension UIViewController {
/// 转场类型,方便后续扩展 @objc public enum TransitioningType: Int { case none = 0 case popup = 1 }
/// 设置转场类型 @objc public var transitioningType: TransitioningType { get { getAssociatedObject() as? TransitioningType ?? .none } set { if newValue == .popup { transitioningDelegate = self.popupTransitioningDelegate modalPresentationStyle = .custom } setAssociatedObject(newValue) } }
/// transitioningDelegate 实现类,需要被持有 private var popupTransitioningDelegate: ZCXPopoup.TransitioningDelegate { lazyVarAssociatedObject { ZCXPopoup.TransitioningDelegate() } } }
|
到这里,一个轻量级的弹窗管理就封装好了。我们就可以给任意一个控制器加上这个交互。
自定义弹框
上面只是封装了弹框的交互,那么我们要怎么实现一个弹框呢?
很简单,具体来说就是,创建一个控制器,将其view
设置成透明,然后在其中间加上弹框内容视图contentView
。然后,设置控制器的transitioningType = .popup
,使用present
方式打开即可。
这里大家可能会问,为什么不直接修改控制器的preferredContentSize
,而是弄了一个背景透明的全屏控制器。这个问题非常好,欢迎留言讨论。
设置转场类型和打开弹框:
1 2 3 4 5 6
| @IBAction func showPopoup(_ sender: Any) { let sb = UIStoryboard(name: "DemoViewController", bundle: nil) guard let controller = sb.instantiateInitialViewController() else { return } controller.transitioningType = .popup present(controller, animated: true) }
|
关闭弹框:
1 2 3 4 5
| class DemoViewController: UIViewController { @IBAction func dismiss(_ sender: Any) { dismiss(animated: true) } }
|

总结
上述方法,可以将弹框的交互独立封装出来,具体的业务弹框只需要实现好UI
和交互事件,以及相应功能即可,弹框的打开和关闭,使用present
和dismiss
即可。
可以看到,弹框交互和业务可以完全解耦,这也是能做到弹框的高度可定制的核心。我们可以将这个交互沉淀到基础库,用来规范项目中弹框的统一交互。
源码
ZCXPopoup
参考
UIPresentationController
UIViewControllerAnimatedTransitioning
UIViewControllerTransitioningDelegate
原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0