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

前言

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

預(yù)備知識(shí)

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

  • UIPresentationController
  • UIViewControllerTransitioningDelegate
  • UIViewControllerAnimatedTransitioning

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

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

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

2衣撬、UIViewControllerTransitioningDelegate定義了轉(zhuǎn)場(chǎng)代理方法,可以指定PresentedDismissed動(dòng)畫扮饶,以及UIPresentationController具练。

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

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

            // 背景蒙層淡入動(dòng)畫
            presentedViewController.transitionCoordinator?.animate { _ in
                self.dimmingView.alpha = 1.0
            }
        }

        override func dismissalTransitionWillBegin() {
            // 背景蒙層淡出動(dòng)畫,以及移除操作
            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
        }()
    }
}

代碼比較簡(jiǎn)單奥帘,主要的工作就是添加了一個(gè)背景蒙層铜邮,以及蒙層的動(dòng)畫交互處理,加上子視圖尺寸的控制。

注:上面的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)
            }
        }
    }
}

這個(gè)實(shí)現(xiàn)類的內(nèi)容也較簡(jiǎn)單牍鞠,主要是設(shè)置轉(zhuǎn)場(chǎng)動(dòng)畫時(shí)長(zhǎng)咖摹,以及實(shí)現(xiàn)轉(zhuǎn)場(chǎng)動(dòng)畫,轉(zhuǎn)場(chǎng)動(dòng)畫分為進(jìn)場(chǎng)(present)和出場(chǎng)(dismiss)動(dòng)畫难述。

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、為控制器增加一個(gè)擴(kuò)展攀芯,方便使用彈框交互

extension UIViewController {

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

    /// 設(shè)置轉(zhuǎn)場(chǎng)類型
    @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() }
    }
}

到這里侣诺,一個(gè)輕量級(jí)的彈窗管理就封裝好了殖演。我們就可以給任意一個(gè)控制器加上這個(gè)交互。

自定義彈框

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

這里大家可能會(huì)問,為什么不直接修改控制器的preferredContentSize宇葱,而是弄了一個(gè)背景透明的全屏控制器瘦真。這個(gè)問題非常好,歡迎留言討論黍瞧。

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

@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ù)可以完全解耦,這也是能做到彈框的高度可定制的核心矢否。我們可以將這個(gè)交互沉淀到基礎(chǔ)庫(kù)仲闽,用來規(guī)范項(xiàng)目中彈框的統(tǒng)一交互。

思考題

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

源碼

ZCXPopup

參考

UIPresentationController
UIViewControllerAnimatedTransitioning
UIViewControllerTransitioningDelegate

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末验庙,一起剝皮案震驚了整個(gè)濱河市顶吮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌粪薛,老刑警劉巖悴了,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異违寿,居然都是意外死亡湃交,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門藤巢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來搞莺,“玉大人,你說我怎么就攤上這事菌瘪∪校” “怎么了?”我有些...
    開封第一講書人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵俏扩,是天一觀的道長(zhǎng)糜工。 經(jīng)常有香客問我,道長(zhǎng)录淡,這世上最難降的妖魔是什么捌木? 我笑而不...
    開封第一講書人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮嫉戚,結(jié)果婚禮上刨裆,老公的妹妹穿的比我還像新娘彬檀。我一直安慰自己帆啃,他們只是感情好窍帝,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般疯坤。 火紅的嫁衣襯著肌膚如雪报慕。 梳的紋絲不亂的頭發(fā)上压怠,一...
    開封第一講書人閱讀 52,785評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音菌瘫,去河邊找鬼蜗顽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛突梦,可吹牛的內(nèi)容都是我干的诫舅。 我是一名探鬼主播宫患,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼娃闲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起卷哩,我...
    開封第一講書人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤属拾,失蹤者是張志新(化名)和其女友劉穎将谊,沒想到半個(gè)月后渐白,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡栋齿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年瓦堵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片菇用。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡陷揪,死狀恐怖泉唁,靈堂內(nèi)的尸體忽然破棺而出揩慕,到底是詐尸還是另有隱情扮休,我是刑警寧澤迎卤,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布玷坠,位于F島的核電站,受9級(jí)特大地震影響八堡,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜缝龄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一挂谍、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧口叙,春花似錦、人聲如沸俺亮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)萨醒。三九已至,卻和暖如春囤踩,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背堵漱。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工涣仿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留示惊,地道東北人愉镰。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像录择,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子隘竭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容