your3i’s blog

iOSエンジニア。頑張る⚉

iOS Custom Presentation & Transition (3) 〜Custom Transition〜

はじめに

iOS Custom Presentation & Transition (1) 〜コード一行もない編〜 - your3i’s blog
iOS Custom Presentation & Transition (2) 〜UIPresentationControllerでカスタムモーダルを作る〜 - your3i’s blog

1は作るものの紹介と大体の概念を説明し、2はpresentation controllerを利用してSemiModalの実装を説明した。今回は引き続き、Custom transitionの実装方法を簡単に説明。

なぜCustom transitionを実装する必要がある

1で作るものを紹介したと思うが、モーダルを出すときは下からslide inして、閉じるときは下にslide outするように。そして、モーダルをdragしても閉じれるように。

前回(2)はここまで作った:

f:id:your3i:20181005132917g:plain

modalTransitionStyleをstoryboardでcover verticalに設定してあるから、slide in slide outの要件はもうすでに満たしてる。なんかcustom transition やらなくてもいいじゃない?

でも実は、dragして閉じれることを作るには、UIPercentDrivenInteractiveTransitionというクラスを利用する必要がある。これのオブジェクトが使われる前提としてcustom transition animatorを作る必要がある。

A percent-driven interactive transition object relies on a transition animator delegate—a custom object that adopts the UIViewControllerAnimatedTransitioning protocol—to set up and perform the animations.

そのため、drag dismiss を実現するために、すでにslide outできるようにしてるけど、それぽい挙動になるcustom transitionを作らなきゃいけない。

下からslide outするcustom transitionの実装

CustomDismissAnimatorを作る

UIViewControllerAnimatedTransitioningをadoptしたクラスCustomDismissAnimatorを作成。今回はdismissのときだけ、custom transitionにしたいから、dismiss専用animatorを作った。Animatorという名前は、別の名前にしてもいいけど、transitionのanimationを定義するクラスだから、animatorにした。

final class CustomDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {
}

UIViewControllerAnimatedTransitioningのメソッドをimplementしてないから、エラーが出ると思う。

まず、transition アニメーションの時間を返さなきゃいけない。

final class CustomDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.2
    }
}

そして、transitionはどんなアニメーションで行われるのかを教えなきゃいけない。

final class CustomDismissAnimator: NSObject, UIViewControllerAnimatedTransitioning {

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 0.2
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    // とりあえず、何もせずtransition終わったようを教えてあげよう
    transitionContext.completeTransition(true)
    }
}

CustomDismissAnimatorを使われるようにする

DetailsViewControllerにUIViewControllerTransitioningDelegateの以下の方法をadoptする。dismissするときこのクラスが定義したアニメーションを使ってくれという意味だ。

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        return CustomDismissAnimator()
    }

動きをみてみると、DetailsViewControllerを閉じるときしゅっと消えるようになった。これはアニメーションちゃんと作ってなかったからだ、でもこれで、CustomDismissAnimatorが使われていることがわかる。

f:id:your3i:20181015213834g:plain

transitionのアニメーション

正直いうとここはまだそんなに理解してないんだ。
これってなに?になるものがいくつかあって、一応自分現状の理解で説明すると。

  • UIViewControllerContextTransitioningのtransitionContext
    • transitionアニメーションが行われる場所としての箱みたいなもの。紙芝居のあの箱みたいな。
  • transitionContext.viewController(forKey: .from)
    • fromとtoのそれぞれのview controllerがある。今回はDetailsViewControllerオブジェクトからMainViewControllerオブジェクトに遷移するため、fromのview controllerはDetailsViewControllerオブジェクト。
  • fromViewの位置の計算
    • 前のステップのgifのように、detailViewControllerのviewであるfromViewはシュッと消えるのように、transitionContextの中で、今fromViewのtransition始まるときの位置と終わるときの位置が一緒。目標はtransitionが終わるとき、fromViewがy軸で下から消えるように下から、改めてfromViewのfinalFrameを計算しておく。
  • transitionContext.completeTransition
    • こっちからcompleteTransitionを指示しなきゃ、transitionContext自分からtransitionを終えることができないみたい。そのため、animationが完了したあと、ちゃんと教える必要がある。そうしないと、transitionContextにはcontainerViewがあって、そのviewが画面上にぞっと残ってて、下のMainViewControllerオブジェクトを触ることができなくなる。

って、コードはこうなる:

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        // DetailsViewControllerオブジェクトを取る
        guard let fromViewController = transitionContext.viewController(forKey: .from),
            let fromView = transitionContext.view(forKey: .from) else {
            transitionContext.completeTransition(true)
            return
        }

        // DetailsViewControllerオブジェクトの最終位置の計算
        var fromFinalFrame = transitionContext.finalFrame(for: fromViewController)
        let newFinalOrigin = CGPoint(x: fromFinalFrame.origin.x, y: UIScreen.main.bounds.height)
        fromFinalFrame.origin = newFinalOrigin

        // アニメーション
        let duration = transitionDuration(using: transitionContext)
        UIView.animate(
            withDuration: duration,
            animations: {
                fromView.frame = fromFinalFrame
            },
            completion: { _ in
                // transitionキャンセルされたかどうか次第で、transitionを完了する
                let success = !transitionContext.transitionWasCancelled
                transitionContext.completeTransition(success)
            }
        )
    }

実装完了

なにも変わってないじゃん〜 はい、そうですw
でもこれで!interactive transitionが作れるようになる!
次回で話す。

f:id:your3i:20181015220910g:plain

おわりに

transitionContextについてちゃんと説明することができなかった🤦‍♀️
でもanimateTransitionの中は、色々おしゃれなアニメーション作れるところだから、よく理解した方が良さそう。今度機会があったら、おしゃれなアニメーションを作ってみてそして書く。