$30 off During Our Annual Pro Sale. View Details »

UIKitによるCustomTransition・SwiftUIでの類似表現&HeroTransition関連に関するよもやま話

 UIKitによるCustomTransition・SwiftUIでの類似表現&HeroTransition関連に関するよもやま話

YUMEMI.grow Mobile #3での登壇資料になります。

以前CustomTransition関連の調査やサンプル実装をした経験から関心があったこともあり、このタイミングで自分自身の復習という意味も込めて簡単に実装方法や方針に関する基本事項を紹介したものになります。

実装手段を改めて比べてみると違いを知る事ができ、そしてその部分が面白くもあるとも感じている次第です。

「UIKitを利用した場合のCustomTransition関連実装のおさらい」を中心に据えて、SwiftUIでは同様の表現を実現するための方針や、AndroidやFlutterでの実現方針と比較すると大きく異なるが考え方は少し似ている点について紹介しています。

Fumiya Sakai

May 07, 2023
Tweet

More Decks by Fumiya Sakai

Other Decks in Technology

Transcript

  1. UIKitによるCustomTransition・SwiftUIでの類似表現

    HeroTransition関連に関するよもやま話
    YUMEMI.grow Mobile #3
    2023/05/10
    Fumiya Sakai

    View Slide

  2. 自己紹介
    ・Fumiya Sakai
    ・Mobile Application Engineer
    アカウント:
    ・Twitter: https://twitter.com/fumiyasac

    ・Facebook: https://www.facebook.com/fumiya.sakai.37

    ・Github: https://github.com/fumiyasac

    ・Qiita: https://qiita.com/fumiyasac@github
    発表者:
    ・Born on September 21, 1984
    これまでの歩み:
    Web Designer
    2008 ~ 2010
    Web Engineer
    2012 ~ 2016
    App Engineer
    2017 ~ Now
    iOS / Android / sometimes Flutter

    View Slide

  3. iOSのUI実装本を執筆しています!
    書籍に掲載したサンプルのバージョンアップや続編等に現在着手中です。
    少しの工夫で実現できるTIPS集やライブラリ表現の活用集をはじめとした、iOSア
    プリ開発の中でも特にUI実装やUIKitを利用した画面の中で特徴を与える様な表現
    という題材に焦点を当てた書籍となっております。
    現在は電子書籍版のみとなります。 こちらは全て¥1,000となっております。
    https://just1factory.booth.pm/
    概要:
    https://book-tech.com/
    価格:
    📖 Booth
    📖 Book Tech

    View Slide

  4. UI実装であると嬉しいレシピブックの最新情報
    UI実装であると嬉しいレシピブックVol.3として昨年10月に商業化しました!
    Still

    WIP
    これまでの同人誌として頒布したものに加えて、Vol.1及びVol.2に頒布したものの
    中で書籍に載せきれなかったものや表現や動きが特徴的でユーザーにもほんの少し
    遊び心を与える様なUI実装を紹介したものをVol.3としています。
    概要:
    これからの構想:
    こちらで購入可能です:
    Amazon / Google Play / Apple Books / KINOKUNIYA / Rakuten BOOKS etc..
    🏊 iOS: SwiftUIを利用したUI実装や動画関連の実装
    🏊 Android: Jetpack Composeの基本やその他気になるUI表現の考察

    View Slide

  5. 今回のスライドにつきまして
    以前CustomTransition関連の調査やサンプル実装をした経験から関心があった
    今回紹介する表現は「自分がiOSアプリの勉強を始めた際の憧れの実装」でもあったことから思い入れがあります。
    1. UIKitを利用した場合のCustomTransition関連実装のおさらい:
    これまでも書籍等を通じて簡単ではあるが、CustomTransitionに関連するサンプル実装に取り組んだ経緯がありました。取り組
    み始めた時は結構苦戦した事もあったので、改めてポイントを整理しておきたいという思いがありました。
    2. SwiftUIでも同様な表現が実現できるか?:
    画面遷移の仕組みはSwiftUIとUIKitでは大きく異なるので、この点を考慮して同様ないしは類似した形の画面遷移Animationの様
    な表現を実現するために必要な部分を押さえておきたいという動機もありました。
    3. AndroidやFlutterではどの様に実現するか?:
    最近ではAndroidやFlutterも少しずつではありますが実務・個人問わず触れた経験があったので、iOSと比べた際にどの様な相違
    点があるかという点も上記の観点同様に押さえておくと難易度やイメージの比較がしやすいと思いました。

    View Slide

  6. CustomTransitionを活用した画面遷移時の事例紹介
    選択したサムネイル画像が浮かび上がる様な画面遷移Animation処理事例
    事例1. Grid形式のLayoutからPush/Pop画面遷移処理 事例2. UIPageViewController等と合わせた遷移処理
    UIKitでは画面遷移時のAnimationを遷移元ないしは遷移先の情報も利用する事で独自にカスタマイズが可能です。

    View Slide

  7. CustomTransitionを利用する事で得られる効果
    画面遷移時表現としての位置付け以上の視覚的な効果も期待できる場合もある
    (参考)過去の登壇資料 ※書籍サンプルと一緒にAnimationを考察したもの : https://www.slideshare.net/fumiyasakai37/ui-172957909
    Why CustomTransition?
    画面遷移時に使われる効果やAnimationを指す
    モバイルアプリでは画面表示スペースが限ら
    れているため、ユーザーに何度も画面遷移を
    してもらう必要がある。
    📝 モバイルアプリ特有の背景として…
    ✨ 効果的な活用でUserのストレスを軽減
    ✨ 無意識に操作方法をに学習させる効果
    機能と体験の調和を感じる例の1つ
    😊 様々なiOS/Androidアプリ内で心地よい・気になる画面遷移があるはず
    (Adobe記事) アプリに最適なアニメーション遷移とスピードを考えてみよう!

    https://blog.adobe.com/jp/publish/2016/08/25/web-xd-ui-transition-easing
    Appleが提供するアプリ内でも美しい画面遷移が散りばめられている

    View Slide

  8. UIKit利用時における基本事項の整理と復習(1)
    CustomTransitionを利用する表現において最低限押さえるべき事項
    UIViewControllerAnimatedTransitioning:
    NSObjectを継承したクラスをまずは準備し、次にこのプロトコルに準拠させる必要がある。
    画面遷移Animationの実体となる部分
    class DetailTransition: NSObject {

    // ※その他画面遷移をカスタマイズする為に必要な変数値を定義する

    // 👉 画面遷移処理を組み立てるに当たって必要な数値や要素を外部から渡せる様にしておくと良いです。

    // 例. トランジションの方向を決定するための値や位置・サイズ情報等。

    }

    // MARK: - UIViewControllerAnimatedTransitioning

    extension DetailTransition: UIViewControllerAnimatedTransitioning {

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

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { … }

    }
    animateTransitionの処理内でクラス内に定義した変数を適用する。
    ① Animation秒数
    ② Animation処理
    https://developer.apple.com/documentation/uikit/uiviewcontrolleranimatedtransitioning

    View Slide

  9. UIKit利用時における基本事項の整理と復習(2)
    連続性のあるAnimation処理の為に画面遷移元&遷移先の情報等を活用する
    // MARK: - UIViewControllerAnimatedTransitioning

    extension DetailTransition: UIViewControllerAnimatedTransitioning {

    // アニメーションの時間を定義する

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

    return 0.28

    }

    // アニメーションの実装を定義する

    // 画面遷移コンテキスト(UIViewControllerContextTransitioning)を利用する 👉 遷移元や遷移先のViewController等の関連情報が格納されている

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {

    // コンテキストを元にViewのインスタンスを取得する(存在しない場合は処理を終了)

    guard let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else { return }

    guard let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) else { return }

    // アニメーションの実体となるContainerViewを作成する

    let container = transitionContext.containerView

    // 👉 CustomTransitionの実体となるContainerViewのAnimation定義をする …

    }

    }
    ① 両方の画面View要素を取得する
    ② ContainerViewへfromView/toViewを場合に応じて追加
    例. 進む遷移: toView / 戻る遷移: fromView を追加
    ③ 加えて必要な値等を適用しAnimation処理を実行する

    View Slide

  10. UIKit利用時における基本事項の整理と復習(3)
    Present/Dismiss&Push/Popの画面遷移を実行する際に最低限押さえるべき事項
    UIViewControllerTransitioningDelegate: Present/Dismiss画面遷移時にCustomTransitionを適用するために必要なもの
    https://developer.apple.com/documentation/uikit/uiviewcontrollertransitioningdelegate
    UINavigationControllerDelegate:
    https://developer.apple.com/documentation/uikit/uiviewcontrollertransitioningdelegate
    Push/Pop画面遷移時にCustomTransitionを適用するために必要なもの
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) ->
    UIViewControllerAnimatedTransitioning?
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
    func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController:
    UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning?
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation,
    from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
    下記の部分に定義したCustomTransition用のクラス定義を適用する(Present/Dismissを分けられる様にしておく)
    下記の部分に定義したCustomTransition用のクラス定義を適用する + Swipe処理時Interaction定義を適用する

    View Slide

  11. UIKit利用時における基本事項の整理と復習(4)
    Present/Dismiss時の画面遷移を実行する際に最低限押さえるべき事項
    // MARK: - UIViewControllerTransitioningDelegate

    extension MainViewController: UIViewControllerTransitioningDelegate {

    // 進む場合のアニメーションの設定を行う

    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController)
    -> UIViewControllerAnimatedTransitioning? {

    // 現在の画面サイズを引き渡して画面が縮むトランジションにする

    newsTransition.originalFrame = self.view.frame

    newsTransition.presenting = true

    return newsTransition

    }



    // 戻る場合のアニメーションの設定を行う

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

    // 縮んだ状態から画面が戻るトランジションにする

    newsTransition.presenting = false

    return newsTransition

    }

    }
    📝 Animation付きCrossFade
    ※ Animationの内容としてはContainerViewをアフィン変換を利用したものである。
    クラス内プロパティで画面遷移の方向を決める(クラス自体を分割してもOK)

    View Slide

  12. UIKit利用時における基本事項の整理と復習(5)
    Push/Pop時の画面遷移を実行する際に最低限押さえるべき事項
    // MARK: - UINavigationControllerDelegate

    extension MainViewController: UINavigationControllerDelegate {

    func navigationController(_ navigationController: UINavigationController, interactionControllerFor animationController: UIViewControllerAnimatedTransitioning)
    -> UIViewControllerInteractiveTransitioning? {

    guard let targetInteractor = detailInteractor else { return nil }

    return targetInteractor.transitionInProgress ? targetInteractor : nil

    }

    }
    UIPercentDrivenInteractiveTransition: 左端をSwipeすることでPop(遷移先から戻る)処理をする際に必要なもの
    https://developer.apple.com/documentation/uikit/uipercentdriveninteractivetransition
    NSObjectを継承したクラスをまずは準備し、次にこのプロトコルに準拠させる必要がある。
    ※ UIPercentDrivenInteractiveTransitionに準拠したクラス内で実施していること。
    UIScreeEdgePanGestureRecognizerの処理と連動させる様にする。
    ※ UIScreeEdgePanGestureRecognizerの処理における、.began /.changed / .cancelled / .ended と連携する
    UIPercentDrivenInteractiveTransitionにおける、update(progress) / cancel() / finish()を実行する

    View Slide

  13. // MARK: - UINavigationControllerDelegate

    extension MainViewController: UINavigationControllerDelegate {

    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC:
    UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {

    // カスタムトランジションのクラスに定義したプロパティへFrame情報とUIImage情報を渡す

    guard let frame = selectedFrame else { return nil }

    guard let image = selectedImage else { return nil }

    detailTransition.originFrame = frame

    detailTransition.originImage = image

    switch operation {

    case .push:

    self.detailInteractor = DetailInteractor(attachTo: toVC)

    detailTransition.presenting = true

    return detailTransition

    default:

    detailTransition.presenting = false

    return detailTransition

    }

    }

    }
    UIKit利用時における基本事項の整理と復習(6)
    Push/Pop時の画面遷移を実行する際に最低限押さえるべき事項
    📝 写真が浮き上がる表現
    ※ CustomTransition用のクラスを適用する
    ※ 左端をSwipeしてPop遷移を実施する処理クラス
    Animationに必要な要素(画像/Frame値)を渡す
    UIPercentDrivenInteractiveTransitionを継
    承したクラスをPushでの画面遷移時に渡す

    View Slide

  14. UIKit利用時における基本事項の整理と復習(7)
    今回紹介した画面表示時における画面遷移表現を実現するコード例
    📝 Animation付きCrossFade 📝 写真が浮き上がる表現
    Animation付きCrossFadeをする様なCustomTransitionの様な実装コード例 (Present / Dismiss)
    https://gist.github.com/fumiyasac/ab396d4938c94115eaa2ecfa71cbc382
    一覧画面からサムネイル画像が浮き上がってくる様なCustomTransitionの様な実装コード例 (Push / Pop)
    https://gist.github.com/fumiyasac/549654724c711e96f9e1fa989931acf1

    View Slide

  15. 従来までのCustomTransition処理で結構難しい点
    必要なProtocolの利用方法を押さえた後も結構シビアな調整が求められる部分
    上記の点以外でも配慮すべき点ではあるものの、実現にあたって必要なものや配慮すべき事項が多いのも特徴。
    1. メソッド名が長くて引数が多い&名前が紛らわしい物が多い:
    最初取り組み始めた時には、まず覚えるのが大変でした…(とはいえ決まった表現であれば利用例から覚えていくのもアリ)
    2. 位置を調整するための処理が複雑になりやすい:
    綺麗な画面遷移間の「繋ぎ目」を実現するために、位置調整のための処理が必要になることが多い
    3. Animationを実現するために必要な値設定や取得が少し面倒なケースもある:
    遷移先・遷移元から必要な要素を取得する必要があるので、配置した画面の実装についても工夫が必要になる
    4. Animationを組み立てる処理において、特に.addSubViewをする場合には気をつける:
    画面遷移Animation用のContainerView内に要素を追加した際は、Animation完了時の後始末を忘れずに実施しなければいけない

    View Slide

  16. View要素内の特定の情報を取得する方法例 :
    画面遷移先からCustomTransitionの要素を取得する
    画面要素のインスタンスを経由して取る以外の方法も考えてみても良いかも
    // 遷移先のViewControllerに配置したUIImageViewのタグ値から、カスタムトランジション時に動かすUIImageViewの情報を取得する

    // ※ 今回はDetailViewController内に配置したtransitionTargetImageViewが該当する

    guard let targetImageView = detailView.viewWithTag(customAnimatorTag) as? UIImageView else {

    return

    }
    CustomTransitionを実現するために遷移先の情報が欲しいので「viewWithTag」を利用して取得する

    View Slide

  17. この様な画面遷移表現を簡単に実現するOSS例
    自前で実装するのは難しい場合はライブラリを上手に活用する道もあります
    (参考)Hero : https://github.com/HeroTransitions/Hero ※ View要素にID付与して連続したAnimationを実現

    View Slide

  18. SwiftUIでこの様な表現をする場合のヒント
    ポイントとなるのは「MatchedGeometryEffect」を利用したAnimation表現
    MatchedGeometryEffect – Part 1 (Hero Animations) :

    https://swiftui-lab.com/matchedgeometryeffect-part1/
    Apple公式ドキュメント:

    https://developer.apple.com/documentation/swiftui/view/matchedgeometryeffect(id:in:properties:anchor:issource:)

    View Slide

  19. MatchedGeometryEffectを利用した実装のイメージ図
    UIKitでの実装方針や考え方とは大きく異なる点には注意が必要かもしれない点
    一覧用View画面 詳細用View画面
    @Namespace private var namespace
    CellView()

    .frame(height: 360.0)

    .contentShape(Rectangle())

    .onTapGesture {

    withAnimation(※Animation処理に関する指定) {

    isExpanded = true

    }

    }
    .overlay {

    if let isExpanded {

    DetailView(productID: $productID, animationID: namespace)

    }

    }
    詳細用View画面に配置したAnimation対象と結び付ける

    ※表示処理時に一意なIDとnamespaceを引き渡す形とする
    .overlay Modifierを利用して詳細用Viewを重ねる
    LazyVGrid等を利用して一覧表示をする
    .matchedGeometryEffect
    一覧表示と詳細表示画面の表示コントロールと一意なID
    @Stateを変更してView要素を再描画
    @State private var productID: String = ``
    @State private var isExpanded: Bool = false
    /

    View Slide

  20. Androidでこの様な表現をする場合のヒント
    JetpackCompose以前は「Shared Element Transition」を利用したAnimation表現
    ユーザ体験を維持した遷移アニメーションの実装:

    https://developers.cyberagent.co.jp/blog/archives/9291/
    アニメーションを使ったアクティビティの開始:

    https://developer.android.com/training/transitions/start-activity?hl=ja

    View Slide

  21. Shared Element Transitionを利用した実装のイメージ図
    画面遷移元と遷移先のxmlに同じtransitionNameを指定しIntentを発行する方針
    一覧用View画面 詳細用View画面
    transitionName = robot
    transitionName = robot
    画面遷移時のIntent発行タイミングで文字列を引き渡す
    ① .makeSceneTransitionAnimationを利用する点がポイント
    ② Animation対象の要素についてはPair.createで複数指定可
    ※下記はActivityのxmlを指定している場合のコードです

    View Slide

  22. Flutterではどの様に実現するか?
    前述したiOSのHeroTransitions(UIKit製OSS)での使い方とよく似ている
    (Flutter公式ドキュメント)Hero animations : https://docs.flutter.dev/ui/animations/hero-animations
    // (1) 画面遷移元

    GestureDetector(

    onTap: () { // 画面遷移先へ進む処理 },

    child: Hero(

    tag: 'imageID_00001',

    child: Image.network('API画像表示用URL'),

    ),

    );
    // (2) 画面遷移先

    GestureDetector(

    onTap: () { // 画面遷移元へ戻る処理 },

    child: Center(

    child: Hero(

    tag: 'imageID_00001',

    child: Image.network('API画像表示用URL'),

    ),

    ),

    );

    View Slide

  23. まとめ
    実装手段を改めて比べてみると違いを知れる。そしてその部分が面白くもある。
    同様ないしは類似した表現における相違点を理解することで比較しながら考える事ができる様に思います。
    1. UIKitを利用した場合のCustomTransition関連実装のおさらい:
    意外と複雑で忘れがちな部分ではあるが、UIKitを利用した際の画面遷移処理のカスタマイズ方法を知った上でその表現を上手に
    応用したり活用する事でアプリ内の心地よい表現やアクセントを加える事も期待できる部分の様に思います。
    2. SwiftUIでは同様の表現を実現するためには「.matchedGeometryEffect」を利用する方針となる:
    SwiftUIでは類似した表現をする場合は、画面遷移の様に見せるためのAnimationをする際に「.matchedGeometryEffect」を利用
    して関連要素を結びつける様な形を取る点が従来までの方法との大きな相違点かと感じています。
    3. AndroidやFlutterでの実現方針と比較すると大きく異なるが関連要素を結び付ける考え方は少し似ている:
    こちらはiOSでの実装方針と大きく異なるものの、Animation対象要素に対して共通するKeyとなる値を遷移元および遷移先に指定
    して画面遷移処理を実行する仕様となっている点がポイントだと個人的に考えております。

    View Slide

  24. Thank you for listening !
    今回はUIKit側の実装解説がメインでしたが、SwiftUI / Android / Flutter側については概要を紹介するだけですみません。

    この部分は自分でも研究するテーマとして今後とも追いかけていきたい部分でもあり、UI実装の面白い部分の1つだと思います。

    View Slide