モーション デザインの詳細: 高度な iOS トランジション

公開: 2020-11-09

クリエイティビティには、ユーザーを感動させたいという絶え間ない欲求が伴います。 何世紀にもわたって、人間は自然を基本的な例として、最も自然な相互作用を再現するために利用可能な手段を操作しようとしてきました.

世界を発見するとき、人は周囲の世界の微妙な詳細にますます敏感になり、人工物と生き物を本能的に区別することができます。 この境界線は、ソフトウェアが、ユーザーが人工的に作成された世界での自分の経験を自然であると説明する環境を作成することを目的とする技術の発展とともにぼやけています。

アプリのデザインに自然を反映

この記事では、ほとんどの iOS アプリケーションにおける日常的な要素のインタラクティブ アニメーションにおける形状変換の例を使用して、境界線をぼかすプロセスを紹介します。 自然を模倣する 1 つの方法は、時間内のオブジェクトの位置をさまざまに変換することです。 アニメーション時間の関数の例を以下に示します。

幾何学的変換を追加することによる適切な時間の使用と組み合わせると、無限の数の効果を得ることができます. 今日のデザインとテクノロジーの可能性を示すデモとして、当社のソフトウェア開発会社が開発した一般的なソリューションを含む Motion Patterns アプリケーションが作成されました。 私はライターではなくプログラマーであり、実例に勝るものはないので、この素晴らしい世界にあなたを招待せざるを得ません。

発見するサンプル

モーションデザインがありふれたデザインを特別なものに変える方法を見てみましょう! 以下の例では、左側に基本的な iOS アニメーションのみを使用するアプリケーションがあり、右側にいくつかの改善を加えた同じアプリケーションのバージョンがあります。

「Dive Deeper」効果

これは、ビューの 2 つの状態間の変換を使用した遷移です。 コレクションに基づいて構築され、特定のセルを選択した後、個々の要素を変換することにより、要素の詳細への移行が行われます*。 追加の解決策は、アプリケーションの使用を容易にするインタラクティブなトランジションの使用です。

*実際には、トランジションに参加している一時的なビューでデータ要素をコピー/マッピングしますが、これについてはこの記事の後半で説明します...

「端から覗く」効果

アクションでスクロール ビュー アニメーションを使用すると、画像が 3D 形式に変換され、立方体効果が得られます。 この効果の主な要因は、スクロール ビューのオフセットです。

「点をつなげる」効果

これは、ミニチュア オブジェクトを画面全体に変換するシーン間の遷移です。 この目的で使用されるコレクションは同時に機能し、一方の画面での変更は他方の画面でのシフトに対応します。 さらに、背景のミニチュアに入ると、シーン間をスワイプすると視差効果が現れます。

「シェイプをシフト」効果

最後のタイプのアニメーションは、Lottie ライブラリを使用した単純なものです。 これは、アイコンをアニメーション化するための最も一般的な用途です。 この場合、これらはタブ バーのアイコンです。 さらに、適切なタブを変更することにより、特定の方向への遷移のアニメーションを使用して、相互作用効果をさらに強化しました。

より深く掘り下げる: 最初のモーション デザイン パターン

いよいよ本題に入ります…これらの例を制御するメカニズムの構造をさらに深く掘り下げる必要があります。

この記事では、「Dive Deeper」と名付けた最初のモーション デザイン パターンについて、具体的な詳細には立ち入らず、その使用方法を抽象的な説明とともに紹介します。 将来的には、正確なコードとリポジトリ全体を制限なしで誰でも利用できるようにする予定です。

プロジェクトのアーキテクチャと適用される厳密なプログラミング デザイン パターンは、現時点では優先事項ではなく、アニメーションとトランジションに重点を置いています。

この記事では、シーンの遷移中にビューを管理するために提供される 2 つの機能セットを使用します。 したがって、この記事は UIKit と Swift の構文に比較的精通している人々を対象としていることを指摘しておきます。

https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning

https://developer.apple.com/documentation/uikit/uipercentdriveninteractivetransition

https://developer.apple.com/library/archive/featuredarticles/ViewControllerPGforiPhoneOS/CustomizingtheTransitionAnimations.html

最初:構造

特定のソリューションを実装する基本バージョンでは、遷移に関係するビューに関する必要な情報を提供し、遷移自体と相互作用を制御する役割を担う、いくつかのヘルパー クラスが必要になります。

Swift での移行

トランジションの管理を担当する基本クラスであるプロキシは、TransitionAnimation になります。 移行がどの方法で行われるかを決定し、Apple チームが提供するアクションを実行するために必要な標準機能をカバーします。

 /// これは、指定された期間にわたって表示と非表示で異なる動作をするトランジションの基本クラスです。
クラス TransitionAnimator を開く: NSObject, UIViewControllerAnimatedTransitioning {
    
    /// トランジションを提示しているか、却下しているかを示します。
    var presenting: Bool = true
    
    /// 遷移全体が発生する時間間隔。
    プライベート レット期間: TimeInterval
    
    /// デフォルト値を持つ遷移アニメーターのデフォルト初期化子。
    /// - パラメータ期間: 遷移全体が発生する時間間隔。
    /// - パラメータ表示: トランジションを表示または非表示にするかどうかを示すインジケーター。
    public init(duration: TimeInterval = 0.5, presenting: Bool = true) {
        self.duration = 期間
        self.presenting = プレゼンテーション
        super.init()
    }
    
    /// 遷移の期間を決定します。
    /// - パラメータ transitionContext: 現在の遷移のコンテキスト。
    /// - 戻り値: アニメーターの初期化時に指定された期間。
    public func transitionDuration(transitionContext を使用: UIViewControllerContextTransitioning?) -> TimeInterval {
        self.duration を返す
    }
    
    /// トランジション アニメーターの心臓部。この関数でトランジションが行われます。
    /// - 重要: アニメーションを実行するには、より具体的なトランジション タイプでこの関数をオーバーライドすることが重要です。
    /// - パラメータ transitionContext: 遷移のコンテキスト。
    public func animateTransition(transitionContext を使用: UIViewControllerContextTransitioning) { }
    
}

TransitionAnimator に基づいて、TransformTransition ファイルを作成します。このファイルのタスクは、特定のトランジション、変換を伴うトランジション (親子関係) を実行することです。

 /// 変換遷移の実装。
クラス TransformTransition を開く: TransitionAnimator {
    
    /// トランジションに必要なすべての仕様を保持するモデルを表示します。
    プライベート var ビューモデル: TransformViewModel
    
    /// 変換遷移のデフォルトのイニシャライザ。
    /// - パラメータ viewModel: 変換遷移のビュー モデル。
    /// - パラメータ duration: トランジションの期間。
    init(viewModel: TransformViewModel、期間: TimeInterval) {
        self.viewModel = ビューモデル
        super.init(期間: 期間、提示: viewModel.presenting)
    }

TransformTransition クラスの構成には、名前が示すように、この遷移が適用されるビュー モデルのメカニズムを通知する TransformViewModel が含まれます。

 /// 基本情報を保持する変換遷移のモデルを表示します。
最終クラスTransformViewModel {
    
    /// トランスフォーム トランジションがビューを表示しているか非表示にしているかを示します。
    提示させてください: Bool
    /// 各ビューの変換に関する仕様を持つモデルの配列。
    let モデル: [TransformModel]
    
    /// 変換ビュー モデルのデフォルトの初期化子。
    /// - Parameter presenting: トランスフォーム トランジションを提示するか非表示にするかを示します。
    /// - パラメータ モデル: 各ビューの変換に関する仕様を持つモデルの配列。
    init(presenting: Bool, models: [TransformModel]) {
        self.presenting = プレゼンテーション
        self.models = モデル
    }
    
}

変換モデルは、親にある遷移に関連するビューの特定の要素を記述する補助クラスです。通常は、変換可能なコントローラーのビューです。

遷移の場合、この遷移は特定の状態間の特定のビューの操作で構成されるため、必要な手順です。

2 番目: 実装

Transformable を使用して遷移を開始するビュー モデルを拡張します。これにより、必要なすべての要素を準備する関数を実装する必要があります。 この関数のサイズは非常に急速に大きくなる可能性があるため、要素ごとなど、小さな部分に分割することをお勧めします。

 /// トランスフォーム遷移を実行したいクラスのプロトコル。
protocol Transformable: ViewModel {
    
    /// 遷移に関与するビューのモデルを準備します。
    /// - パラメータ fromView: 遷移が開始されるビュー
    /// - パラメータ toView: 遷移先のビュー。
    /// - パラメータ表示: 表示中か非表示中かを示します。
    /// - 戻り値: 各ビューのトランジションを変換する準備ができているすべての必要な情報を保持する構造体の配列。
    func prepareTransitionModels(fromView: UIView, toView: UIView, presenting: Bool) -> [TransformModel]
    
}

前提は、変換に参加するビューのデータを検索する方法を言うことではありません。 私の例では、特定のビューを表すタグを使用しました。 実装のこの部分は自由に行うことができます。

特定のビュー変換 (TransformModel) のモデルは、リスト全体で最小のモデルです。 それらは、開始ビュー、遷移ビュー、開始フレーム、終了フレーム、開始中心、終了中心、同時アニメーション、および終了操作などの主要な変換情報で構成されます。 ほとんどのパラメータは変換中に使用する必要がないため、独自のデフォルト値があります。 最小限の結果を得るには、必要なものだけを使用するだけで十分です。

 /// デフォルト値を持つ変換モデルのデフォルト初期化子。
    /// - パラメータ initialView: 遷移が開始されるビュー。
    /// - パラメータ phantomView: 変換遷移中に表示されるビュー。
    /// - パラメータ initialFrame: 変換遷移を開始するビューのフレーム。
    /// - パラメータ finalFrame: 変換遷移の最後に表示されるビューのフレーム。
    /// - パラメータ initialCenter: 最初のビューの中心点が最初のビューの中心と異なる場合に必要です。
    /// - パラメータ finalCenter: ビューの最終中心点が最終ビューの中心と異なる場合に必要です。
    /// - パラメータ parallelAnimation: 変換遷移中に実行されるビューの追加のアニメーション。
    /// - パラメータ補完: 変換遷移後にトリガーされるコードのブロック。
    /// - 注: 変換遷移の最小限のバージョンを実行するには、最初のビューのみが必要です。
    init(initialView: UIView,
         ファントムビュー: UIView = UIView(),
         initialFrame: CGRect = CGRect(),
         finalFrame: CGRect = CGRect(),
         initialCenter: CGPoint? =ゼロ、
         finalCenter: CG ポイント? =ゼロ、
         parallelAnimation: (() -> Void)? =ゼロ、
         完了: (() -> ボイド)? = ゼロ) {
        self.initialView = initialView
        self.phantomView = ファントムビュー
        self.initialFrame = initialFrame
        self.finalFrame = finalFrame
        self.parallelAnimation = parallelAnimation
        self.completion = 完了
        self.initialCenter = initialCenter ?? CGPoint(x: initialFrame.midX, y: initialFrame.midY)
        self.finalCenter = finalCenter ?? CGPoint(x: finalFrame.midX, y: finalFrame.midY)
    }

ファントム ビューに注意が向けられた可能性があります。 今回は iOS トランジションのワークフローについて説明します。 最短の形で…

iOS トランジションのワークフロー

ユーザーが次のシーンに移動したい場合、iOS は、開始 (青) およびターゲット (緑) コントローラーをメモリにコピーすることにより、特定のコントローラーを準備します。 次に、2 つのシーン間のトランジション ビューをシミュレートする以外に、特別な機能を含まない「愚かな」ビューであるコンテナを含むトランジション コーディネーターを通じて、トランジション コンテキストが作成されます。

トランジションを操作する際の重要な原則は、トランジション コンテキストに実際のビューを追加しないことです。これは、トランジションの最後に、すべてのコンテキストがコンテナーに追加されたビューと共に割り当て解除されるためです。 これらは、移行中にのみ存在し、その後削除されるビューです。

したがって、実際のビューのレプリカであるファントム ビューを使用することは、この移行に対する重要なソリューションです。

この場合、形状とサイズを変更することによって、あるビューを別のビューに変換するトランジションがあります。 これを行うには、遷移の開始時に、指定された要素の PhantomView を作成し、コンテナーに追加します。 FadeView は、トランジション全体に柔らかさを加えるための補助ビューです。

 /// トランジションが実行されるトランスフォーム トランジションの中心。 `TransitionAnimator.animateTransition(...)` のオーバーライド。
    /// - パラメータ transitionContext: 現在の変換遷移のコンテキスト。
    open func animateTransition をオーバーライドします (transitionContext を使用: UIViewControllerContextTransitioning) {
        ガードレット toViewController = transitionContext.view(forKey: .to),
            let fromViewController = transitionContext.view(forKey: .from) else {
                log.unexpectedState() を返す
        }
        let containerView = transitionContext.containerView
        let duration = transitionDuration(using: transitionContext)
        let fadeView = toViewController.makeFadeView(不透明度: (!presenting).cgFloatValue)
        let models = viewModel.models
        presentedView = プレゼンティングをさせますか? toViewController : fromViewController
        
        models.forEach { $0.initialView.isHidden = true }
        presentView.isHidden = true
        containerView.addSubview(toViewController)
        {を提示する場合
            containerView.insertSubview(fadeView, belowSubview: toViewController)
        } そうしないと {
            containerView.addSubview(fadeView)
        }
        containerView.addSubviews(viewModel.models.map { $0.phantomView })

次のステップでは、変換によってターゲット シェイプに変換し、それがプレゼンテーションかリコールかに応じて、特定のビューをクリーンアップするための追加の操作を実行します。これが、この遷移のレシピ全体です。

 let animations: () -> Void = { [弱い自己] in
            Guard let self = self else { return Log.unexpectedState() }
            fadeView.alpha = self.presenting.cgFloatValue
            models.forEach {
                let center = self.presenting ? $0.finalCenter : $0.initialCenter
                let transform = self.presenting ? $0.presentTransform : $0.dismissTransform
                $0.phantomView.setTransformAndCenter(変形、中央)
            }
            models.compactMap { $0.parallelAnimation }.forEach { $0() }
        }
        
        let 補完: (Bool) -> Void = { _ in
            transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
            presentView.isHidden = false
            models.compactMap { $0.completion }.forEach { $0() }
            models.forEach { $0.initialView.isHidden = false }
            if !self.presenting && transitionContext.transitionWasCancelled {
                toViewController.removeFromSuperview()
                fadeView.removeFromSuperview()
            }
        }
        
        UIView.animate(withDuration: 期間、
                       遅延: 0,
                       SpringWithDamping の使用: 1、
                       初期バネ速度: 0.5,
                       オプション: .curveEaseOut、
                       アニメーション: アニメーション、
                       完了:完了)

3つ目:こだわりの食材

すべての関数、クラス、およびプロトコルをまとめると、結果は次のようになります。

移行の最後のコンポーネントは、完全な双方向性です。 この目的のために、コントローラー ビュー、TransitionInteractor に追加されたパン ジェスチャを使用します…

 /// インタラクティブな遷移を処理するメディエーター。
最終クラス TransitionInteractor: UIPercentDrivenInteractiveTransition {
    
    /// 遷移が開始されたかどうかを示します。
    var hasStarted = false
    /// 遷移が終了するかどうかを示します。
    var shouldFinish = false

}

…これもコントローラー本体で初期化します。

 /// コレクション ビュー アイテムのパン ジェスチャを処理し、トランジションを管理します。
    @objc func handlePanGesture(_gestureRecognizer: UIPanGestureRecognizer) {
        let percentThreshold: CGFloat = 0.1
        let translation = GestureRecognizer.translation(in: ビュー)
        verticalMovement = translation.y / view.bounds.height を許可します。
        let upperMovement = fminf(Float(verticalMovement), 0.0)
        let upperMovementPercent = fminf(abs(upwardMovement), 0.9)
        進みましょう = CGFloat(upwardMovementPercent)
        ガードレットインタラクター=インタラクションコントローラーelse {リターン}
        ジェスチャRecognizer.stateを切り替えます{
        ケースの開始:
            interactor.hasStarted = true
            let tapPosition = GestureRecognizer.location(in: collectionView)
            showDetailViewControllerFrom(場所: tapPosition)
        ケースが変更されました:
            interactor.shouldFinish = 進行状況 > percentThreshold
            interactor.update(進行状況)
        ケース.キャンセル:
            interactor.hasStarted = false
            インタラクター.キャンセル()
        ケース.終了:
            interactor.hasStarted = false
            interactor.shouldFinish
                ? インタラクター.finish()
                : インタラクター.キャンセル()
        デフォルト:
            壊す
        }
    }

準備完了の相互作用は次のようになります。

すべてが計画通りに進んだ場合、アプリケーションはユーザーの目にはるかに多くの利益をもたらします。

氷山の頂点だけを発見

次回は、モーションデザイン関連の実装について、次号以降で解説していきます。

アプリケーション、デザイン、およびソース コードは Miquido の所有物であり、才能のあるデザイナーとプログラマーによって情熱を持って作成されたものであり、実装での使用については責任を負いません。 詳細なソース コードは、今後 github アカウントから入手できるようになります。ぜひフォローしてください。

ご清聴ありがとうございました。