「ビズリーチ」iOSアプリでは、自社製Redux基盤とRxSwiftで構築された既存実装から、SwiftUIとThe Composable Architecture(TCA)への完全移行を進めています。本記事では、なぜ移行を決断したのか、グロース開発を止めずに安全に移行するための戦略、そして移行によって得られた恩恵を紹介します。

こんにちは。「ビズリーチ」の求職者様向けプロダクトでiOSアプリ開発を担当している森田です。

iOSアプリ開発は、技術の移り変わりがとても激しい領域です。2014年のSwift登場以降に限っても、MVCからの脱却を目指したアーキテクチャの模索、RxSwiftに代表されるリアクティブプログラミングの全盛、Redux系状態管理の流行、そして2019年のSwiftUI登場からSwift Concurrencyの定着まで、ベストプラクティスは数年単位で塗り替えられてきたと思います。

「ビズリーチ」iOSアプリも例外ではありません。かつて最適解として選んだ技術スタックが、開発速度と品質の足かせになっていました。今回は私たちがその負債とどう向き合い、どう返済しているかを記してみようと思います。同じような課題を抱えるモバイルエンジニアの参考になれば幸いです。

背景と課題

技術負債と向き合ってきたこれまで

「ビズリーチ」iOSアプリは2015年にリリースされ、長年の運用を経たプロダクトです。既存のアーキテクチャは、Redux(ReSwift)とRxSwiftを軸に、次のような工夫が凝らされた設計でした。

これらは当時の知見を結集した真っ当な設計だったと思います。しかし数年が経ち、前提が大きく変わりました。RxSwiftのコミュニティはCombineやSwift Concurrencyの登場で縮小傾向にあると言われており、SwiftUIは実プロダクトで十分使える水準に成熟しました。設計当時を知るメンバーは減り、独自基盤の詳細を把握している人がいない状態に近づいていきました。

そもそも技術負債とは何か。私たちの場合、それは「当時の最適解が時代に置いていかれた姿」でした。

RxSwiftに依存した自社製Redux基盤の限界

既存アーキテクチャの画面実装を、概念コードで示すとこうなります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 概念コード: 既存アーキテクチャの画面実装
final class JobDetailViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        // ユースケースは「呼びっぱなし」。ここでは結果を受け取らない
        actionDispatcher.fetchJobDetail(id: jobID)

        // 結果はグローバルなStoreの更新として届く。
        // ViewModelがStoreを監視し、RxSwiftのストリームに変換して流してくる
        viewModel.jobDetail
            .drive(onNext: { [weak self] in self?.render($0) })
            .disposed(by: disposeBag)
    }
}

単方向データフロー自体は健全な考え方です。問題は、その実現手段が自社製・独自ルールの組み合わせだったことにあります。

実際に、この状態管理の複雑さが一因となった不具合は少なくなく、その多くはユニットテストがあれば防げた可能性が高いものでした。

自社製UIフレームワークの限界

UI構築には、SwiftUIライクな書き味をUIKit上で実現する自社製フレームワーク「BizStack」(BizVStack、BizHStackなど)を使っていました。SwiftUIがまだ採用できなかった時代に、宣言的UIの生産性を先取りするために生まれたものです。

しかしSwiftUIが成熟した現在、BizStackは機能面で純正の下位互換になってしまいました。状態バインディング、アニメーション、プレビューなど、SwiftUIなら標準で手に入る機能を自前で再開発しなければならず、SwiftUIなら数行で済む実装に何日もかかることさえありました。これは正直つらかったです。独自フレームワークゆえに社外に知見はなく、学習コストはチームの新メンバーがそのまま背負うことになります。

状態管理とUI構築という土台の両方が限界を迎えており、部分的な改修ではなく抜本的な移行が必要だと判断しました。

移行戦略の全体像

移行先はSwiftUI × TCA

UI構築は自社製フレームワークからApple純正のSwiftUIへ、状態管理は自社製Redux基盤からThe Composable Architecture(TCA)へ移行することに決めました。

TCAを選んだ理由は大きく3つあります。

  1. Reduxに近い概念: 単方向データフローやReducerといった概念はそのまま活きるため、チームがこれまで培ってきた設計の考え方を捨てずに移行できます
  2. 状態と副作用の管理を仕組みで強制できる: 状態は画面単位のStateに閉じ、副作用はEffectとして明示的に扱われます。グローバルStoreの「どこから何が変わるか分からない」問題が構造的に起きません
  3. テストのしやすさ: TestStoreを使うと「Actionを送ったら、Stateがこう変わり、この副作用が走る」という一連の流れを網羅的に検証できます

例えば求人の「気になる」登録は、Reducerとして次のように書けます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Reducer
struct JobDetail {
    @ObservableState
    struct State: Equatable {
        let job: Job
        var isInterested = false
    }

    enum Action {
        case interestButtonTapped
        case interestResponse(Bool)
    }

    @Dependency(\.jobClient) var jobClient

    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .interestButtonTapped:
                return .run { [id = state.job.id] send in
                    await send(.interestResponse(await jobClient.markInterested(id)))
                }
            case let .interestResponse(isInterested):
                state.isInterested = isInterested
                return .none
            }
        }
    }
}

このReducerに対するユニットテストはこう書けます。依存する通信処理はDependencyとして差し替えられるため、テストのために特別な準備は必要ありません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Test
func 気になるを登録できる() async {
    let store = TestStore(initialState: JobDetail.State(job: .mock)) {
        JobDetail()
    } withDependencies: {
        $0.jobClient.markInterested = { _ in true }
    }

    await store.send(.interestButtonTapped)
    await store.receive(\.interestResponse) {
        $0.isInterested = true
    }
}

私たちは「状態を持つ画面を移行する際は、必ずユニットテストを実装する」ことをプロジェクトの要求事項として定めました。

無理せず安全に進める

実は「ビズリーチ」では、Web版とAndroid版が先行して大規模なリアーキテクチャを完了しています(Android版の取り組みはこちらの記事で紹介しています)。これらがグロース開発を一時停止して集中的に置き換える「ビッグバン」型だったのに対し、iOSでは画面単位で少しずつ置き換える段階移行を選びました。移行期間中も新旧のアーキテクチャが共存し、日々のリリースは止まりません。

段階移行を支えるのが、切り戻し可能なリリースの仕組みです。

  1. 移行した画面ごとにFirebase Remote Configのフラグを用意し、新旧の実装をアプリ内に並置します
  2. まずは少数のユーザーにのみ新画面を解放し、ABテストの要領で新旧の行動指標を比較します
  3. 「◯%悪化したら」ではなく「統計的に有意なマイナス影響が出たら切り戻す」を撤退基準とします。新旧で挙動が同じなら差は出ないはずで、有意差が出ている時点で何かがおかしいからです
  4. 指標に問題がなければ100%に展開し、約1ヶ月のモニタリングを経てRemote Configフラグと旧実装コードを削除します

また、QAチームと共同でテスト計画を作り、ユニットテスト→開発者テスト→画面ごとのQAテスト→リリース前の全体リグレッションテストという多段のテスト工程を設けています。異なるデバイスサイズとOSバージョンの組み合わせも計画的にカバーしています。

現実的なSwiftUI導入

全画面を一度にSwiftUI化するのは非現実的です。私たちはUIKitとの共存を前提に、依存関係の少ない末端画面から着手しました。

また、最初の3ヶ月は仮説検証フェーズと位置づけ、状態が少なく遷移も単純な設定系の画面から着手しました。ここで移行パターンと画面規模ごとの工数実績を得られたため、以降のフェーズでは精度の高い計画を立てられるようになりました。

グロース開発と並行して進める

リアーキテクチャ単体では売上を直接生みません。だからこそ、「グロース施策を止めない」ことを前提条件とし、通常のスプリントに移行作業を織り込んで進めています。

移行フェーズごとに、削減できる調査工数・不具合対応工数・AI活用による効果を積み上げてROIを試算し、プロダクトオーナーの合意を得てから着手しています。技術負債の返済をROIで説明できるプロジェクトとして運営することが、継続の鍵だと考えています。

こうして、仮説検証→求人関連画面→メッセージ・登録導線と、対象領域を広げながら移行を進めてきました。

得られた恩恵

保守・メンテナンスの難易度が大幅に下がった

最も大きな変化は、意図せぬ副作用で苦しむ生活とおさらばできたことです。状態が画面単位のStateに閉じたことで、「この画面の修正が、どこか別の画面を壊すかもしれない」という不安から解放されました。修正の影響範囲が構造的に限定されるため、コードリーディングにかかる調査時間も大きく減っています。

ユニットテストの存在も効いています。移行した画面にはTestStoreによる網羅的なテストが揃っており、仕様変更やリファクタリングも、動くことをテストが保証している状態で進められます。移行済み画面では、これまでQAテストで検出されていたような状態管理起因の不具合が実装段階で検知できるようになり、品質改善とQA負荷の軽減につながっています。

実装パターンの統一も、日々の開発を楽にしています。「新しい画面はこのパターンで作る」という共通認識ができ、既存コードが最良のサンプルコードとして機能するようになりました。SwiftUIとTCAはいずれも公開された学習リソースが豊富なため、新しくチームに加わるメンバーのオンボーディングも格段に容易になりました。

AIとの相性が良く、新しいツールを試しやすい環境に

移行を進める中で想定以上に効いてきたのが、コーディングAIとの相性です。

旧実装は独自フレームワークの塊で、その文脈はAIの学習データに存在しません。AIに実装を任せても独自ルールに沿わないコードが生成され、実務投入は困難でした。一方、新実装を構成するSwiftUIとTCAは世界中で使われている技術です。責務分離が明確で変更が局所的に閉じ、生成されたコードの正しさをユニットテストで機械的に検証できます。これはコーディングAIが力を発揮するための条件がそのまま揃った構成です。

土台が標準的な技術になったことで、新しいツールの検証も気軽になりました。Figma MCPやGitHub MCPといった周辺ツールも小さく試しながら取り込めるようになり、開発環境を継続的にアップデートしやすくなりました。

おわりに

「ビズリーチ」iOSアプリのSwiftUI × TCAへの完全移行について、その背景と戦略、得られた恩恵を紹介しました。

移行はまだ道半ばです。設定・求人・メッセージ・登録導線と主要な領域の移行を進めてきましたが、プロフィールタブなどの画面群が残っており、旧基盤を完全に撤去するまで取り組みは続きます。

それでも、移行を進めた領域では確かな手応えがあります。意図せぬ副作用に怯えることなく、テストとAIに支えられて開発できる。私自身、数年前のこのアプリからは想像しづらかった開発体験です。

技術負債の返済は、当時の最適解に敬意を払いながら、プロダクトの未来のために土台を作り直す投資だと考えています。この記事が、同じように負債と向き合うチームのヒントになれば嬉しいです。こうした環境でのiOSアプリ開発に少しでも興味を持っていただけたら、ぜひお話ししましょう!

DroidKaigi 2026に協賛・登壇します

株式会社ビズリーチは、2026年9月1日(火)〜9月3日(木)にベルサール渋谷ガーデンで開催される DroidKaigi 2026 に協賛し、ブース出展を行います。

また9月2日(水) 14:20より、弊社の加藤が「アクセシビリティを利用するとき、アクセシビリティもまたこちらを利用している 〜マルウェアによる攻撃と防衛について〜」というタイトルで登壇します。AccessibilityServiceを悪用したマルウェアの手口と、アプリ側で取りうる防御策についてお話しする予定です。

ブースでは、ノベルティをご用意しているほか、iOS / Android両方のエンジニアが参加予定です。プラットフォームをまたいだ開発の話ができればうれしいです。ご来場の際は、ぜひお立ち寄りください!

ビズリーチでは、新しい仲間を募集しています。

お客様にとって価値あるモノをつくり、働く環境の変革に挑戦する仲間を募集しています。
募集中のポジションやプロダクト組織の詳細は、ぜひキャリア採用サイトをご覧ください。

ビズリーチ採用サイト
森田 久真(moriq)
森田 久真(moriq)

iOSアプリ開発エンジニア。ゴルフとサウナが趣味。