Swift實(shí)現(xiàn)一個交互友好&靈活自定義的彈框

前言

在我們平時日常開發(fā)中,經(jīng)常會遇到各種樣式的彈框。你是否也經(jīng)常遇到呢?你是如何實(shí)現(xiàn)的?
本文介紹使用UIPresentationController,結(jié)合自定義轉(zhuǎn)場動效,實(shí)現(xiàn)一個高度自定義的彈框,這也是蘋果比較推薦的一種實(shí)現(xiàn)方式。

預(yù)備知識

開始之前,我們要了解下幾個知識點(diǎn):

  • UIPresentationController
  • UIViewControllerTransitioningDelegate
  • UIViewControllerAnimatedTransitioning

1、UIPresentationController是什么?官方文檔中介紹如下:

An object that manages the transition animations and the presentation of view controllers onscreen.

簡單來說,它可以管理轉(zhuǎn)場動畫和模態(tài)出來的窗口控制器。詳細(xì)信息可以參考:UIPresentationController文檔

2、UIViewControllerTransitioningDelegate定義了轉(zhuǎn)場代理方法,可以指定PresentedDismissed動畫,以及UIPresentationController

3、UIViewControllerAnimatedTransitioning就是轉(zhuǎn)場動畫協(xié)議,我們可以遵守該協(xié)議,實(shí)現(xiàn)轉(zhuǎn)場動畫。

實(shí)現(xiàn)

1、自定義UIPresentationController,并實(shí)現(xiàn)相應(yīng)方法

struct ZCXPopup {}

extension ZCXPopup {

    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
        }()
    }
}

代碼比較簡單,主要的工作就是添加了一個背景蒙層,以及蒙層的動畫交互處理,加上子視圖尺寸的控制。

注:上面的ZCXPopup結(jié)構(gòu)體沒有實(shí)際作用,僅僅是為了區(qū)分命名空間。

2、UIViewControllerAnimatedTransitioning實(shí)現(xiàn)類實(shí)現(xiàn)

extension ZCXPopup {

    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)
            }
        }
    }
}

這個實(shí)現(xiàn)類的內(nèi)容也較簡單,主要是設(shè)置轉(zhuǎn)場動畫時長,以及實(shí)現(xiàn)轉(zhuǎn)場動畫,轉(zhuǎn)場動畫分為進(jìn)場(present)和出場(dismiss)動畫。

3、UIViewControllerTransitioningDelegate實(shí)現(xiàn)類實(shí)現(xiàn)

extension ZCXPopup {

    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)
        }
    }
}

在該實(shí)現(xiàn)類中,實(shí)現(xiàn)代理方法,分別返回自定義的PresentationControllerTransitionAnimator即可。

4、為控制器增加一個擴(kuò)展,方便使用彈框交互

extension UIViewController {

    /// 轉(zhuǎn)場類型,方便后續(xù)擴(kuò)展
    @objc public enum TransitioningType: Int {
        case none  = 0
        case popup = 1
    }

    /// 設(shè)置轉(zhuǎn)場類型
    @objc public var transitioningType: TransitioningType {
        get { getAssociatedObject() as? TransitioningType ?? .none }
        set {
            if newValue == .popup {
                transitioningDelegate = self.popupTransitioningDelegate
                modalPresentationStyle = .custom
            }
            setAssociatedObject(newValue)
        }
    }

    /// transitioningDelegate 實(shí)現(xiàn)類,需要被持有
    private var popupTransitioningDelegate: ZCXPopup.TransitioningDelegate {
        lazyVarAssociatedObject { ZCXPopup.TransitioningDelegate() }
    }
}

到這里,一個輕量級的彈窗管理就封裝好了。我們就可以給任意一個控制器加上這個交互。

自定義彈框

上面只是封裝了彈框的交互,那么我們要怎么實(shí)現(xiàn)一個彈框呢?
很簡單,具體來說就是,創(chuàng)建一個控制器,將其view設(shè)置成透明,然后在其中間加上彈框內(nèi)容視圖contentView。然后,設(shè)置控制器的transitioningType = .popup,使用present方式打開即可。

這里大家可能會問,為什么不直接修改控制器的preferredContentSize,而是弄了一個背景透明的全屏控制器。這個問題非常好,歡迎留言討論。

設(shè)置轉(zhuǎn)場類型和打開彈框:

@IBAction func showPopup(_ sender: Any) {
    let sb = UIStoryboard(name: "DemoViewController", bundle: nil)
    guard let controller = sb.instantiateInitialViewController() else { return }
    controller.transitioningType = .popup
    present(controller, animated: true)
}

關(guān)閉彈框:

class DemoViewController: UIViewController {
    @IBAction func dismiss(_ sender: Any) {
        dismiss(animated: true)
    }
}
Popup.gif

總結(jié)

上述方法,可以將彈框的交互獨(dú)立封裝出來,具體的業(yè)務(wù)彈框只需要實(shí)現(xiàn)好UI和交互事件,以及相應(yīng)功能即可,彈框的打開和關(guān)閉,使用presentdismiss即可。
可以看到,彈框交互和業(yè)務(wù)可以完全解耦,這也是能做到彈框的高度可定制的核心。我們可以將這個交互沉淀到基礎(chǔ)庫,用來規(guī)范項目中彈框的統(tǒng)一交互。

思考題

點(diǎn)擊彈框空白區(qū)域關(guān)閉彈框,這個處理放在哪里實(shí)現(xiàn)更合適?歡迎留言討論。

源碼

ZCXPopup

參考

UIPresentationController
UIViewControllerAnimatedTransitioning
UIViewControllerTransitioningDelegate

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容