Slide 1

Slide 1 text

UIKitによるCustomTransition・SwiftUIでの類似表現 HeroTransition関連に関するよもやま話 YUMEMI.grow Mobile #3 2023/05/10 Fumiya Sakai

Slide 2

Slide 2 text

自己紹介 ・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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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表現の考察

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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が提供するアプリ内でも美しい画面遷移が散りばめられている

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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処理を実行する

Slide 10

Slide 10 text

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定義を適用する

Slide 11

Slide 11 text

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)

Slide 12

Slide 12 text

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()を実行する

Slide 13

Slide 13 text

// 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での画面遷移時に渡す

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

View要素内の特定の情報を取得する方法例 : 画面遷移先からCustomTransitionの要素を取得する 画面要素のインスタンスを経由して取る以外の方法も考えてみても良いかも // 遷移先のViewControllerに配置したUIImageViewのタグ値から、カスタムトランジション時に動かすUIImageViewの情報を取得する // ※ 今回はDetailViewController内に配置したtransitionTargetImageViewが該当する guard let targetImageView = detailView.viewWithTag(customAnimatorTag) as? UIImageView else { return } CustomTransitionを実現するために遷移先の情報が欲しいので「viewWithTag」を利用して取得する

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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:)

Slide 19

Slide 19 text

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 /

Slide 20

Slide 20 text

Androidでこの様な表現をする場合のヒント JetpackCompose以前は「Shared Element Transition」を利用したAnimation表現 ユーザ体験を維持した遷移アニメーションの実装: https://developers.cyberagent.co.jp/blog/archives/9291/ アニメーションを使ったアクティビティの開始: https://developer.android.com/training/transitions/start-activity?hl=ja

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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'), ), ), );

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

Thank you for listening ! 今回はUIKit側の実装解説がメインでしたが、SwiftUI / Android / Flutter側については概要を紹介するだけですみません。 この部分は自分でも研究するテーマとして今後とも追いかけていきたい部分でもあり、UI実装の面白い部分の1つだと思います。