首頁 Swift 自訂轉場動畫,手勢(下)
文章
取消

Swift 自訂轉場動畫,手勢(下)

Demo GIF


這篇主要講手勢的使用,關於動畫實作的部分,在 上篇 有詳細的code

接下來要在我們建立的 CustomTransition 裡面,跟系統說:我們的互動事件要透過誰來管理互動。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CustomTransition: NSObject, UIViewControllerTransitioningDelegate {

    // ...

    let interaction = UpToDownIneraction()

    func interactionControllerForPresentation(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        return nil
    }


    func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
        print(interation.interacting)
        return interaction.interacting ? interaction : nil
    }
}

這邊使用 interaction.interacting ? interaction : nil 這樣的寫法,翻成中文大概的意思是:如果已經在處理互動了,那就不用多嘴跟系統說誰來處理互動。所以在 UpToDownIneraction 裡面我們宣告一個 interacting 來負責是否在互動中。

注意這兩個 func 在 UIViewControllerTransitioningDelegate 裡面其實是 optional 的,如果不實做的話,系統會使用原生的手勢,ex: navigation 會有右滑返回的手勢。回傳 nil 同理。 ** 那這邊只使用 interactionControllerForDismissal ,也就是 dismiss 需要的透過誰來管理。因為在出現的部分如果使用手勢,不熟悉的使用者可能會產生疑惑,除非UI上有明顯的提示,例如一個箭頭,加上一個抖動的動畫,不然使用點擊事件來當作出現的觸發條件會是比較好的選擇。


接下來就是實作互動管理了

先把 class 建立起來,並寫好我們需要的 property
1
2
3
4
5
6
7
class UpToDownIneraction: UIPercentDrivenInteractiveTransition {

    var interacting: Bool = false

    private var couldComplete: Bool = false
    private weak var presentingViewController: UIViewController? = nil
}

細心的人可能發現我們繼承的 class 是 UIPercentDrivenInteractiveTransition 而不上面 func 需要回傳的 UIViewControllerInteractiveTransitioning 這是蘋果提供的一個方便的類別,如果動畫中使用的是 UIView.animation,那我們就只需要告訴系統現在要 run 到第幾 % 。

  • interacting 是管理是否在互動中

  • couldComplete 負責的是,如果滑到一半,就放開手指,這時候是要跑完整個動畫,還是恢復動畫前的狀態。

  • presentingViewController 是我們需要加上手勢的 ViewController

    (除了配合手勢,執行動畫,還需要跟系統說我們要 dismiss 還是 pop,所以需要一個 property 存起來方便之後判斷)

然後實作我們需要的方法

首先加上手勢,並做好手勢需要執行的空方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class UpToDownIneraction: UIPercentDrivenInteractiveTransition {

    // ...

    func wireGesture(on viewController: UIViewController) {
        presentingViewController = viewController
        let gesture = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
        viewController.view.addGestureRecognizer(gesture)
    }

    @objc func handleGesture(_ gestureRecoginizer: UIPanGestureRecognizer) {
        // ...
    }
}

接著實作手勢執行的方法, UIPercentDrivenInteractiveTransition 類別提供了我們幾個方法

  • update(_ percentComplete: CGFloat)
  • cancel()
  • finish()

很明顯我們只需要算好手勢在畫面上移動的比例,然後使用 update 來更新,並在適當的時機跟系統說我們要 取消 或是 完成 動畫,即可。

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
class UpToDownIneraction: UIPercentDrivenInteractiveTransition {

    // ...

    @objc func handleGesture(_ gestureRecoginizer: UIPanGestureRecognizer) {
        let gestureView = gestureRecoginizer.view!
        let trainsiton = gestureRecoginizer.translation(in: gestureView)
        switch gestureRecoginizer.state {
        case .began:
            print("began")
            interacting = true
            if let naviController = presentingViewController?.navigationController {
                naviController.popViewController(animated: true)
            } else {
                presentingViewController?.dismiss(animated: true, completion: nil)
            }
        case .changed:
            var fraction = trainsiton.y / gestureView.frame.height
            print("\(fraction)")
            fraction = max(fraction, 0.0)
            fraction = min(fraction, 1)
            couldComplete = fraction > 0.4
            update(fraction)
        case .cancelled, .ended:
            interacting = false
            if couldComplete == false || gestureRecoginizer.state == .cancelled {
                cancel()
            } else {
                finish()
            }
        default:
            break
        }
    }
}

484很簡單?這邊特別提醒一下,如果動畫不是使用 UIView.animation 來實作,那麽 UIPercentDrivenInteractiveTransition 就不適用了。這個坑害我搞了好久。

萬事俱備只欠東風

1
2
3
4
5
6
7
8
@objc func buttonPressed() {
    let vc = PresentSecondViewController()
    animation.destinationPoint = button.center
    vc.transitioningDelegate = animation
    vc.modalPresentationStyle = .custom
    animation.interaction.wireGesture(on: vc)
    present(vc, animated: true, completion: nil)
}

完整的範例在 Github

本文由作者按照 CC BY 4.0 進行授權