your3i’s blog

iOSエンジニア。頑張る⚉

Heroを使ったmodal viewcontrollerをドラッグ閉じるの実装

この間、画像のプレビュー画面を作った。

よくある、フルスクリーンのズームイン・ズームアウトできる画面。

閉じるときは、一応×ボタンで閉じれる。

でも、やっぱりTwitterみたいに、下スワイプして閉じれる方がかっこいいだね。

 

Heroとは

HeroはTransitionをいい感じにするライブラリー。
GitHub - lkzhao/Hero: Elegant transition library for iOS & tvOS


実装

例えば、ImageViewerViewControllerがある。

 f:id:your3i:20180430135652p:plain:w300


Screen Bをドラッグして、背景は透明になっていく、最後Screen Aになるというinteractiveなtransitionを作る。
f:id:your3i:20180430140019p:plain:w260


Screen BのUI構造としては:
f:id:your3i:20180430141438p:plain:w300


そして、

Step #1 Heroをimport
import Hero
Step #2
override func viewDidLoad() {
        super.viewDidLoad()
        hero.isEnabled = true // このviewControllerがhero使えるようにする

        // transition中透明になっていくので、animation typeをfadeに設定
        hero.modalAnimationType = .fade
}
Step #3 PanGestureRecognizerをviewにつける
override func viewDidLoad() {
        super.viewDidLoad()
        hero.isEnabled = true
        setupPanGesture()
}

private func setupPanGesture() {
        let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:)))
        view.addGestureRecognizer(panGesture)
}
Step #4 Gestureをハンドリング
@objc func handlePanGesture(_ gestureRecognizer: UIPanGestureRecognizer) {
        let translation = panGesture.translation(in: nil)
        
        // transitionがどこまで進んだのかのprogessを計算。
        // 上スワイプしても閉じれるようにしたいから、absを使う
        let progress = abs(translation.y / 2) / view.bounds.height

        switch panGesture.state {
        case .began:
                // transition を始める。
                hero.dismissViewController()
        case .changed:
                // transitionのprogressを更新
                Hero.shared.update(progress)

                // このままじゃ、ただだんだんfadeして画面が閉じられるだけ
                // 下にdragすると、画像も下にいくべき
                // scrollviewのpositionを変える。
                let currentPosition = CGPoint(x: scrollView.center.x, y: translation.y + scrollView.center.y)
                Hero.shared.apply(modifiers: [.position(currentPosition)], to: scrollView)
        default:
                let velocity = panGesture.velocity(in: nil).y

                // dismiss完成の条件のチェック
                if progress + abs(velocity) / view.bounds.height > 0.5 {
                      // transition animationがfadeだから、scrollviewのtarget positionが元々のposition
                      // transitionが自動的に完了する間も、scrollview(要するに画像)が続いて下に移動して消えていくを実現するために、
                      // scrollviewのtarget position を変える必要がある
                      // そのため、下ドラッグと上ドラッグそれぞれの場合のpositionを計算。
                      let destinationY: CGFloat = {
                      if velocity >= 0 {
                          return view.bounds.height + scrollView.bounds.size.height / 2
                      } else {
                          return -scrollView.bounds.size.height / 2
                      }
                  }()
                      
                  // そして、「scrollviewの最終状態が変わったよ」ってHeroに伝える
                  // Heroがtargetのpositionによって、続きのtransitionの間で、
                  // scrollviewをいい感じにanimateしてくれる
                  let destinationPos = CGPoint(x: scrollView.center.x, y: destinationY)
                  Hero.shared.changeTarget(modifiers: [.position(destinationPos)], to: scrollView)
                  Hero.shared.finish(animate: true)
              } else {
                  // dismissの完成条件が足りなかったら、transitionをキャンセル
                  Hero.shared.cancel()
              }
          }
    }