tomoyaonishiのブログ

iOSのことを中心に・・・その他もあるよ!

UIVIewControllerカスタムトランジションを理解する 1

UIViewControllerのカスタムトランジション

UIViewControllerのカスタムトランジションってめっちゃ複雑じゃないないですか?

実際にやってみるとわりと簡単なんですが、クラスがいろいろあったり、デリゲートメソッドも似たようなものが多くて、UIViewControllerAnimatedTransitioningとかUIViewControllerTransitioningDelegateとか、「すみません、もう一度お願いします」みたいなのが多すぎてさらに混乱するんでしょね。

あとはAppleのサンプルコードもネット上のサンプルコードも超絶どシンプルなものがないのも要因かと。

OSSもオーバーキルなものが多いし、ごちゃごちゃやってて結局わかりづらい。

一見ごちゃごちゃしているのですが、汎用性、再利用性も高く設計されていて「めっちゃええやん..」ってなります。

というわけで、カスタムトランジションのシンプルなサンプルを書いたので自由にコピペして使ってください

UIViewでも簡単にできますが、UIViewControllerベースのほうがライフサイクルもしっかりしていて、最近のAppleの主流?(UIAlertControllerとか)にも合っていていいかなと思います。


簡単な解説

大きく3つくらいの利用目的があるかなと思います。

  • present時の遷移アニメーションを変更したい(遷移アニメーションの変更
  • present時のpresentされるVCの表示位置、大きさ、また背景にブラーをつけるなどをしたい(present状態そのものの変更
  • インタラクティブにVCをpresent or dismissさせたい

注意ですが、アニメーションとプレゼンテーションを明確に意識してください。

アニメーションは遷移時に下から出てくるのか、右から出てくるのか、フェードインなのかということです。

プレゼンテーションはどこにどの大きさで表示されるか、背景はどうするかなど、遷移アニメーション後の状態のことを指します。

present時の遷移アニメーションだけを変更したい

modalPresentationStyleを自作するイメージです。 やることは3つです。

1.UIVC.transitioningDelegateをどこかにセットする。

    override func awakeFromNib() {
        super.awakeFromNib()
        transitioningDelegate = transitionController
    }

2.present or dismissあるいは両方のデリゲートメソッドを実装し、アニメーションオブジェクトを返す

final class CustomAnimationTransitions: NSObject, UIViewControllerTransitioningDelegate {

    // これらのメソッドはpresent() or dismiss()の直後に呼ばれる。

    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        // nilを返すとでデフォルトのアニメーションできる
        return AnimationController(isPresenting: true)
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        // すべてoptionalのprotocolなのでpresent時だけ変えたいならこのメソッド自体不要
        return AnimationController(isPresenting: false)
    }

3.アニメーションオブジェクトを実装する

    final class AnimationController: NSObject, UIViewControllerAnimatedTransitioning {
        let isPresenting: Bool

        init(isPresenting: Bool) {
            self.isPresenting = isPresenting
        }

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

        func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
            // ここにアニメーションを書く!
            if isPresenting {
                animateForPresentation(context: transitionContext)
            } else {
                animateForDismissal(context: transitionContext)
            }
        }

        func animateForPresentation(context: UIViewControllerContextTransitioning) {
            guard let destination = context.viewController(forKey: .to) else {
                return
            }

            let containerView = context.containerView
            destination.view.transform.ty = containerView.frame.maxY
            containerView.addSubview(destination.view) // 遷移先のViewを準備する

            let timing = UISpringTimingParameters(dampingRatio: 0.5)
            let animator = UIViewPropertyAnimator(duration: transitionDuration(using: context), timingParameters: timing)
            animator.addAnimations {
                destination.view.transform = .identity
            }
            animator.addCompletion { (position) in
                context.completeTransition(position == .end)
            }
            animator.startAnimation()
        }

        func animateForDismissal(context: UIViewControllerContextTransitioning) {
            guard let from = context.viewController(forKey: .from) else { return }
            let containerView = context.containerView

            UIView.animate(withDuration: transitionDuration(using: context),
                           animations: {
                            from.view.transform.ty = -containerView.bounds.height
            },
                           completion: { finished in
                            context.completeTransition(true)
            })
        }
    }

これだけでpresent時のアニメーションを書き換えることができました。

クラスが別れているので別のVCでも使いたいときは簡単に使えますね。

present時のpresentされるVCの表示位置、大きさ、また背景にブラーをつけるなどをしたい

次はカスタムプレゼンテーションです。アニメーションは変更せず、表示位置を変えたり、大きさを変えたりできます。

アニメーションとは無関係です。

アラート、アクションシート、サイドバーなどを自作したいときに使えます。

やることは3つです。

1.UIVC.transitioningDelegateをどこかにセットする。

解説済みなので省略

2.デリゲートメソッドを実装し、UIPresentationControllerオブジェクトを返す

present時の状態を変更するものなので、アニメーションの変更のようにdismiss時のメソッドはありません。

final class CustomPresentationTransitions: NSObject, UIViewControllerTransitioningDelegate {

    func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? {
        return PresentationController(presentedViewController: presented, presenting: presenting)
    }

}

3.UIPresentationControllerオブジェクトを実装する

final class PresentationController: UIPresentationController {

        override var frameOfPresentedViewInContainerView: CGRect {
            let frame = super.frameOfPresentedViewInContainerView
            return frame.inset(by: UIEdgeInsets(top: 100, left: 50, bottom: 100, right: 50))

            // サイドバーのようにしたいとき
            // return CGRect(x: frame.width / 2, y: frame.origin.y, width: frame.width, height: frame.height)
        }

        override func containerViewWillLayoutSubviews() {
            super.containerViewWillLayoutSubviews()
            presentedView?.frame = frameOfPresentedViewInContainerView
        }
    }

これで全画面のpresentではなくて一部に表示するようなことができます。

カスタムトランジションのうち、カスタムアニメーションとカスタムプレゼンテーションを解説しました。

紹介したクラスはほかにも様々なプロパティやメソッドをもっているので調べてみてください。

またアニメーションやプレゼンテーションの仕方によってはViewControllerの参照を持っておいたりする必要が出てくると思います。

カスタムアニメーションとカスタムプレゼンテーションの合わせ技で好きなアニメーションで好きなpresent状態にできます。

またインタラクティブトランジションでドラッグして閉じるとかもできますが、これら2つはまたの機会にまとめます。

ソースコード(著作権ありません、自由にコピペしてください。)

github.com