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

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

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.

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を楽しみましょう! ご清聴ありがとうございました!