Upgrade to Pro — share decks privately, control downloads, hide ads and more …

SwiftUI Transaction を徹底活用!ZOZOTOWN UI開発での活用事例

SwiftUI Transaction を徹底活用!ZOZOTOWN UI開発での活用事例

Avatar for Ryo Tsuzukihashi

Ryo Tsuzukihashi

June 09, 2025
Tweet

More Decks by Ryo Tsuzukihashi

Other Decks in Technology

Transcript

  1. © ZOZO, Inc. 株式会社ZOZO ZOZOTOWN開発本部 ZOZOTOWN開発2部 iOSブロック 續橋 涼 2019.4: ヤフー

    2023.2: DeNA 2024.8: ZOZO - 個人開発が趣味 - 30個以上App Storeリリース 2 好きなマックスくん
  2. © ZOZO, Inc. https://zozo.jp/ 3 • ファッションEC • 1,600以上のショップ、9,000以上のブランドの取り扱い •

    常時107万点以上の商品アイテム数と毎日平均2,700点以上の新着 商品を掲載(2025年3月末時点) • ブランド古着のファッションゾーン「ZOZOUSED」や コスメ専門モール「ZOZOCOSME」、シューズ専門ゾーン 「ZOZOSHOES」、ラグジュアリー&デザイナーズゾーン 「ZOZOVILLA」を展開 • 即日配送サービス • ギフトラッピングサービス • ツケ払い など
  3. © ZOZO, Inc. 4 メイク投稿コンテンツのZOZOTOWN連携の背景 ZOZOTOWNの特徴 「商品」軸のECサイト ファッションアイテムを販売 WEARの特徴 「人」軸のコンテンツプラットフォーム

    ユーザーが自身のコーデやメイクを投稿 目的 ZOZOTOWNユーザーが商品探しの参考にメイク投稿を活用できるようにすること 対象 ベースメイク・メイクアップ関連カテゴリーに限定 x 連携条件 特定のユーザー種別( WEARISTA/著名人、ショップスタッフ等)の投稿のみ 一部ショップ限定
  4. © ZOZO, Inc. 5 既存コーデ投稿表示機能 技術負債とユーザー体験の課題 UIKit & Combineの実装 WEARコーデ投稿機能はほぼUIKitとCombineで実装

    今のZOZOTOWNの採用アーキテクチャと異なっていた モーダル画面の問題 下スワイプで閉じられない ユーザーストレス 閉じることが容易にできないと、開くこともしなくなる
  5. © ZOZO, Inc. SwiftUI導入の動機と新たな挑戦 リッチなユーザー体験 自然なアニメーションの実現 標準アーキテクチャ MVVM+UseCase テスタブルな実装 UnitTestを書きやすい設計

    ユーザー体験へのこだわり: ・下スワイプで閉じられる自然な操作感 ・画像拡大時に元の場所からスムーズに拡大し、閉じる際も元の場所へ戻る 6
  6. © ZOZO, Inc. 8 実装途中の遷移アニメーション やりたいこと • 下から上に行くアニメーション無くす • 画像を元の位置から動くように表示させる

    • 表示領域が変わる分のSafeAreaInsetsの計算する • Viewerの中で画面を閉じるときは逆の動きを行う
  7. © ZOZO, Inc. 9 SwiftUI Transaction の活用 デフォルト無効化 標準トランジションを一時的に無 効化し、カスタムアニメーション

    を優先 細かい制御 モーダル遷移のライフサイクルと 画像アニメーションを同期・制御 する Transaction SwiftUIのビュー更新とアニメー ションの「コンテキスト」を伝播 ・制御する仕組み 明示的な制御 複雑な要素が絡むアニメーション に、明示的な制御を行う
  8. © ZOZO, Inc. 10 実装の詳細:Transactionによる制御 画像タップ時の処理 Transactionを作成し、disablesAnimations = trueに設定 fullScreenCoverの標準アニメーションを無効化

    画像自身のアニメーション SafeAreaInsetsを考慮した移動アニメーションを行う 画像クローズ時の処理 拡大画面を閉じるときでもTransactionを使用 Viewerが非表示になる際の標準アニメーションを無効化
  9. © ZOZO, Inc. 11 画像タップ時の処理 .onTapGesture { var transaction =

    Transaction() // Transactionの作成 transaction.disablesAnimations = true // 遷移アニメーションの無効化 withTransaction(transaction) { // 遷移時の処理 showImageDetail.toggle() // 画像Viewerを表示 } }
  10. © ZOZO, Inc. 12 実装の詳細:Transactionによる制御 画像タップ時の処理 Transactionを作成し、disablesAnimations = trueに設定 fullScreenCoverの標準アニメーションを無効化

    画像自身のアニメーション SafeAreaInsetsを考慮した移動アニメーションを行う 画像クローズ時の処理 拡大画面を閉じるときでもTransactionを使用 Viewerが非表示になる際の標準アニメーションを無効化
  11. © ZOZO, Inc. 13 画像自身のアニメーション @Binding var showImageDetail: Bool @Binding

    var transform: CGAffineTransform // 常に位置、大きさなどの情報を同期させておく @Binding var bottomInsets: CGFloat // FullScreenModalになることによるSafeAreaの考慮 . . image .scaleEffect( x: transform.scaleX, // extensionで作ってる y: transform.scaleY, anchor: .zero ) .offset(x: transform.tx, y: transform.ty) .offset(y: -bottomInsets) .opacity(showImageDetail ? 1 : 0)
  12. © ZOZO, Inc. 14 CGAffineTransform の extension extension CGAffineTransform {

    static func anchoredScale(scale: CGFloat, anchor: CGPoint) -> CGAffineTransform { .init(translationX: anchor.x, y: anchor.y) .scaledBy(x: scale, y: scale) .translatedBy(x: -anchor.x, y: -anchor.y) } var scaleX: CGFloat { sqrt(a * a + c * c) } var scaleY: CGFloat { sqrt(b * b + d * d) } }
  13. © ZOZO, Inc. 15 実装の詳細:Transactionによる制御 画像タップ時の処理 Transactionを作成し、disablesAnimations = trueに設定 fullScreenCoverの標準アニメーションを無効化

    画像自身のアニメーション SafeAreaInsetsを考慮した移動アニメーションを行う 画像クローズ時の処理 拡大画面を閉じるときでもTransactionを使用 Viewerが非表示になる際の標準アニメーションを無効化
  14. © ZOZO, Inc. 16 画像クローズ時の処理 func dismissImage() async { //

    0.2秒で前画面の画像の高さの位置に戻す withAnimation(.easeInOut(duration: 0.2)) { bottomInsets = -bottomInsets } try? await Task.sleep(nanoseconds: 200 * 1000 * 1000) // 0.1秒で背景色を透明にしていく withAnimation(.easeInOut(duration: 0.1)) { showAnimation = false } try? await Task.sleep(nanoseconds: 100 * 1000 * 1000) // 遷移アニメーションを無効にする var transaction = Transaction() transaction.disablesAnimations = true withTransaction(transaction) { showImageDetail = false } }
  15. © ZOZO, Inc. 17 Viewerでの画像操作 複合ジェスチャー • DragGesture 画像の平行移動 •

    MagnifyGesture 表示されている位置を中心にズーム • SpatialTapGesture 画像をタップしたとき、タップした位置にズームする
  16. © ZOZO, Inc. 18 DragGestureの制御 .onChanged { value in //

    デフォルト倍率のとき、上下方向のスワイプで画像の大きさ、背景色を変更する if (lastTransform.scaleX == 1) && (lastTransform.scaleY == 1) { let verticalOffset = abs(adjustedTranslation.height) / contentSize.height let scale = max(0.8, 1.0 - verticalOffset * 0.7) let newTransform = lastTransform .translatedBy( x: adjustedTranslation.width / transform.scaleX, y: adjustedTranslation.height / transform.scaleY ) transform = newTransform backgroundOpacity = max(0.0, 1.0 - verticalOffset)    }
  17. © ZOZO, Inc. 19 DragGestureの制御 } else { // ズームしている状態なら、普通に並行移動する

    transform = lastTransform.translatedBy( x: value.translation.width / transform.scaleX, y: value.translation.height / transform.scaleY ) }
  18. © ZOZO, Inc. 20 DragGestureの制御 .onEnded({ value in // デフォルト倍率のとき

    if (lastTransform.scaleX == 1) && (lastTransform.scaleY == 1) { let verticalOffset = abs(value.translation.height) / contentSize.height // 規定値より上下にスワイプしていたら閉じる if verticalOffset > 0.3 { Task { await dismissImage() } } else { // 規定値より上下にスワイプしていないなら状態を元に戻す transform = .identity lastTransform = .identity backgroundOpacity = 1.0 } } else {
  19. © ZOZO, Inc. 21 DragGestureの制御 } else { // デフォルト倍率でないなら、

    onEndGesture() } }) // Gesture終了後の共通処理 private func onEndGesture() { let newTransform = limitTransform(transform) transform = newTransform lastTransform = newTransform }
  20. © ZOZO, Inc. 22 MagnifyGesture MagnifyGesture() .onChanged { value in

    let newTransform = CGAffineTransform.anchoredScale( scale: value.magnification, anchor: .init( x: value.startAnchor.x * contentSize.width, y: value.startAnchor.y * contentSize.height ) ) transform = lastTransform.concatenating(newTransform) } .onEnded { value in onEndGesture() }
  21. © ZOZO, Inc. 23 SpatialTapGesture SpatialTapGesture(count: 1) .onEnded { value

    in let newTransform: CGAffineTransform = if transform.isIdentity { .anchoredScale(scale: 3, anchor: value.location) } else { .identity } transform = newTransform lastTransform = newTransform }
  22. © ZOZO, Inc. 25 まとめ 1. 自然なUX ユーザーにとって自然で滑らかな画像拡大・縮小体験を提供 2. 一貫性

    下スワイプによるモーダルクローズも解決 一貫したUXを実現 3. モダン化 UIKit時代の負債を解消、モダンなSwiftUIアーキテクチャで堅牢なUI構築 Transactionは複雑なUIアニメーションの制御に不可欠なツール 標準APIの限界を超え、カスタムUI/UXを実現する強力な手段
  23. © ZOZO, Inc. 26 今後の展望と感謝 ZOZOTOWN iOS開発の今後 • WWDCの最新技術をキャッチアップしてSwiftUIの可能性を最大限に引き出す •

    常にユーザー体験を第一に考え、技術的挑戦を続ける • WWDC25を楽しみましょう! ご清聴ありがとうございました!