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

10年超のiOSアプリをSwift6へ

 10年超のiOSアプリをSwift6へ

■ イベント
食べログ × ANDPAD × Sansan × ピクシブ モバイル勉強会 #5
https://sansan.connpass.com/event/406876/

■登壇概要
タイトル:10年超のiOSアプリをSwift6へ
登壇者:技術本部 Engineering Unit Mobile Applicationグループ 劉 志輝

■ 技術本部 採用情報
https://media.sansan-engineering.com

Avatar for SansanTech

SansanTech PRO

October 07, 2026

More Decks by SansanTech

Other Decks in Technology

Transcript

  1. 劉 志輝(Liu Zhihui) 技術本部 Engineering Unit Mobile Application Group 2013年からiOS・Androidのモバイルアプリケーション開発に従事し、

    10年以上 の実務経験を持つ。 2025年にSansan株式会社へ入社し、 iOSエンジニアとして Sansanモバイルアプ リの開発・改善を担当。 現在はMobile Architectとして、モバイルアプリの設計方針、技術基盤、開発プ ロセスの改善を推進している。 趣味はキャンプで、富士山周辺によく行く
  2. 本日のアジェンダ - 背景と課題 — 長寿アプリに積み重なった非同期処理 - 方針 — 安全な段階移行 -

    実践 — コードとリリース、その後の変化 - 学び — 移行で得たこと
  3. 複数の非同期処理技術が共存するようになった 10年以上、機能開発を止めずに技術基盤を更新してきた結果、 4つの非同期処理技術が混在した GCD / OperationQueue 実行制御 SwiftQueue RxSwift ジョブ管理

    イベント合成 async / await 言語レベルの 並行処理 現在のSansan iOSアプリ 1つの機能を追うために、異なるキャンセル・エラー処理・ライフサイクル管理を横断 Swift 6では、async/await以外の3つがSendable警告の発生源になる
  4. Swift 6移行にあたって、数千件の警告が発生していた データ競合をコンパイル時に防ぐために Swift 6へ移行する。 その代わり、数千件の警告をすべて直す必要があった MainActor関連 移行の目的 SCC complete

    データ競合による クラッシュや 不具合を防ぐ 数千件の警告 Swift 6切り替え前に検査 主にUI関連 Sendable関連 非同期処理技術をまたぐ境界
  5. 課題1: RxSwiftがSCCと根本的に相性が悪い AnyObserverやObservableは、そのままでは Sendableではない RxSwift × SCC complete Sendableクロージャ内でのキャプチャ .uploadProgress

    { progress in // warning: Capture of 'observer' with non-sendable type // 'AnyObserver<UploadState>' in a `@Sendable` closure observer.onNext(.uploading(progress)) } SCCを有効にすると @Sendableクロージャ内で キャプチャ警告が大量に発生 @preconcurrency import RxSwift 警告は抑制できるが、安全性の検査も弱まる
  6. 課題2: RxSwiftとConcurrencyの混在で保守性が下がった RxSwiftがConcurrencyと混在して冗長なラッパが生まれる RxSwift版 async処理をSingleでラップ func deletePost(conversationID: String, postID: String)

    -> Single<Void> { return .create { observer in let task = Task { [weak self] in guard let self else { return } let result = await self.conversationUseCase.deletePost( conversationID: conversationID, postID: postID ) switch result { case .success: observer(.success(())) case let .failure(error): observer(.failure(error)) } } return Disposables.create { task.cancel() } } } ブリッジコード async呼び出し1回を Singleで包み直す オレンジ行=包む処理 二重修正 SCC対応とRx移行を 別々に進めると、 同じ箇所を二度直す
  7. コードの境界に合わせて移行戦略を立てた 同じコードを二度直さないため、境界に合わせて進め方を変える 独立モジュール 巨大なメインターゲット コンパイル境界で進める 機能境界で進める Module A Module B

    機能A 機能B 機能C Module C Module D 機能D 機能E 機能F モジュールごとに SCCのcompleteモードを適用 RxSwift・GCD移行と SCCを機能グループごとに同時対応
  8. 依存方向に沿って 5フェーズに分解した 下位層から上位層へ。振る舞いを変えるのは最後 3. Interactor 依存方向に沿って 下位層から 複数処理を 構 化

    2. UseCase 業務処理を async化 1. API async throwsの 通信経路 4. Presenter 5. Cleanup 呼び出し経路を 切り替え 旧Rx/GCD経路と 移行用コード を削除
  9. 壊さずに切り替える安全装置 戻せることを、実装と QAの両方で保証する 1 並行実装 Swift Concurrency RxSwift版を残して async版を追加 2

    Deprecation First 旧APIへの新規依存を 警告で止める 3 Feature Flag 機能別フラグと 全体停止フラグで切り替え 新経路(ON) Feature Flag ON / OFF QAはONとOFFの 両方を確認 RxSwift 旧経路(OFF) View
  10. AIに任せたのは判断ではなく反復だった 設計成果物は、コードだけでなく再現可能な判断基準 Human 判断を決める • 移行順序と境界の設計 • Task・actor・Flagの規約 Migration Guide

    パターン・制約 完了条件 • 何を同じ挙動とみなすか AI 繰り返す • 依存関係の調査と作業分割 • ガイドに沿った大量変換 • ビルドログからSCC修正 人間が差分と検証結果をレビュー
  11. プロンプトをチームの実行基盤へ変えた 一度の良いプロンプトより、何度でも同じ結果を出せる仕組み Guide Skill Review パターンを文書化 手順を実行単位にする 人間が確認 • Before

    / After • 命名 • Task管理 • SCC対応 Worktree準備 変換 ビルド 失敗 修正 ビルドが通るまで繰り返す ガイドを更新 • 差分の意図 • 振る舞い
  12. 事例1 待つコードを、待てるコードへ 同期的に止める仕組みそのものが不要になった Before RxSwift + DispatchSemaphore After async/await func

    getSettings() throws -> Settings { let semaphore = DispatchSemaphore(value: 0) var result: Result<Settings, Error>? api.fetch().subscribe( onSuccess: { result = .success($0) semaphore.signal() }, onFailure: { result = .failure($0) semaphore.signal() } ).disposed(by: disposeBag) semaphore.wait() return try result!.get() } func getSettings() async throws -> Settings { try await api.fetch() } 不要になったもの DispatchSemaphore 共有する Result DisposeBag スレッドを止めず、失敗は throwsで伝わる
  13. 事例2 オペレーターを処理の意図へ戻した 処理の関係が、上から読めるコードになった 並行取得 逐次依存 combineLatest → async let flatMap

    → 順番どおりの await async let profile = fetchProfile() async let cards = fetchCards() return try await (profile, cards) let user = try await fetchUser() let posts = try await fetchPosts(user.id) return (user, posts) fetchProfile() fetchUser() fetchCards() fetchPosts(user.id) 時間 同時に開始し、両方の完了を待つ 時間 前の結果を使って次を開始する
  14. 事例3 Presenterを切り替え点にした Presenterで初めて、実際の呼び出し経路を切り替える Presenter 切り替え点 @MainActor func load() { if

    concurrencyEnabled { Task { [weak self] in do { guard let value = try await self?.interactor.fetch() else { return } self?.view?.display(value) } catch is CancellationError { return } catch { self?.view?.show(error) } } .store(in: taskBag) } else { loadRx() } } @MainActor UI更新をメインactorへ隔離 Feature Flag 新旧経路を切り替える Taskのキャンセル 画面のライフサイクルに合わせる [weak self] await前にselfを強参照しない
  15. 全機能をリリースし、旧経路を消してから Swift 6へ 旧経路を先に消してから、 Swift 6言語モードへ切り替えた 2026年3月 6月22日 8月17日 中間地点

    グループごとに移行 Cleanup Swift 6へ リリース API〜Interactor は移行済み Feature Flagで戻せる状態 を保つ 全グループの検証が完了し たら次へ メインターゲットで Swift 6言語モードと SCC completeを有 効化 Swift 6で リリース Presenterを移 行中 Feature Flag、 分岐、移行対象の 旧RxSwift経路を 削除 Cleanupまでは戻せる状態を維持し、 以降は一方向に進める
  16. 開発を止めずに、 Rxの使用量を約 3分の2減らした データ層の Rxは0になり、残りは画面側のイベント処理( RxCocoa)だけになった Swift 6の警告 Rxを使う行 Flagによる切り替え分岐

    並行した機能開発 PR 0件 66%減 0箇所 949本 移行前は数千件 2,751行から931行へ 60案件(移行PRは283本) 機能開発を止めずに約 10か月 品質:戻して直せた • 2025年10月着手、2026年8月17日に出荷 • ロールバック3回 • Feature Flagで1%から100%へ段階展開 • 出荷後の新規クラッシュは3件 • SCC起因のhotfixは1件
  17. 長寿アプリの移行で得た 3つの学び 移行の成果は「ゼロにした量」ではなく、次の変更を安全にしたこと 01 02 03 段階移行 AI実行基盤 戻せるリリース 移行は依存方向と

    人間の判断を ロールバックを コードの境界に沿って ガイドとSkillにし、 実装、QA、運用まで 分ける AIの反復を再現可能にする 含めて設計する 次の変更を安全にする
  18. AIで開発は くなったが、 QAがボトルネックになった 実装のスピードに手動 QAが追いつかず、今後は自動テストが重要になる 開発 QA AIで加 手動で滞留 実装と移行を

    確認が追いつかず 届けるまでの時間が どんどん進められた リリース待ちが増えた 縮まらない リリース 度が頭打ち 今後:自動テストを増やし、 QAの手作業を減らす