見出し画像

The Composable Architectureを使ってみた

iOSアプリの開発ではMVCやMVP、MVVMなどのGUIアーキテクチャが採用されることが多いが、宣言的UIフレームワーク(同様のフレームワークでJetpackComposeやFlutter、ReactNativeなどがある)であるSwiftUIの登場により、SwiftUI時代に合ったアーキテクチャを検討する必要が出てきた。今回はSwiftUI開発で使えるThe Composable Architectureというアーキテクチャを簡単に紹介する。

宣言的UIとは?

宣言的UIでは、アプリのステータスに応じて、どのようなUIになるべきかを宣言的に記述する。また、リアクティブなデータバインディングで実現されるため、データドリブンな方法と言える。そのため、データの流れやステータスの管理が重要になり、モデル層以下がしっかり設計されたアーキテクチャが必要となる。

The Composable Architectureとは?

The Composable Architecture (TCA)は、2020年にPoint-Freeのサービス提供者たちによって提案されたアーキテクチャーであり、FluxやReduxの考え方をもとにSwiftやAppleプラットフォームで使いやすいように開発されたもの。

FluxはFacebook社が提案したWebアプリ向けのアーキテクチャで、MVCなどでは複雑化しがちなデータの流れを単一方向にまとめた単方向なデータフローを実現する。一方で、ReduxはFluxをもとに設計されたライブラリの1つで、より厳格な制約(三原則)を設けている。

TCAでは、Reduxと同様にStore, State, Action, Reducerがあり、Viewは直接Stateを更新できなく、Actionを発行してReducerがStoreのStateを更新する流れとなっている。さらに、副作用を表すEffectや依存関係を保持するEnvironmentがある。

基本的な使い方

TCAを使って簡単な時計アプリを作ってみた。

インストール

TCAはSwift Package Managerに対応しているのでFile > Add PackagesからこちらのURLを入力すると、TCAを取得してインストールできる。

次にStateとActionを定義して、ReducerでActionに対応した処理を追加する。今回は時計アプリなので、定期的に時刻表示を更新する必要があるため、タイマー処理のEffectを追加している。最後にViewでStore変数を定義し、bodyの中でViewStoreのプロパティを参照したり、ViewStore#send()でアクションを発行すればよい。

State,Actionの定義

アプリや機能で管理・参照するState、発生するActionを定義する。

/// 時制
enum HourClock {
    /// 12時制 (AMPM)
    case twelve
    /// 24時制
    case twentyFour
}

enum AMPM {
    case am
    case pm
}

/// State
struct ClockState: Equatable {
    /// タイマー起動中かどうか
    var isTimerActive = false
    /// 現在時刻 (HH:mm:ss)
    var time = "00:00:00"
    /// 時制
    var hourClock: HourClock = .twentyFour
    /// AMPM
    var ampm: AMPM? = nil
}

/// Action
enum ClockAction: Equatable {
    /// 時計スタート
    case clockStarted
    /// 時計の動作
    case clockTicked
    /// 時制の切り替え
    case hourClockToggled
}

/// Environment
struct ClockEnvironment {
    var mainQueue: AnySchedulerOf<DispatchQueue>
}

Reducer

各Actionに対応する処理を記述し、その処理内でStateを更新する。また、必要に応じてAPIリクエストなどのEffectを返却する。

import Foundation
import ComposableArchitecture

let clockReducer = Reducer<ClockState, ClockAction, ClockEnvironment> {
  state, action, environment in
    struct timerId: Hashable {}
    
    switch action {
    // 時計スタート
    case .clockStarted:
        state.isTimerActive.toggle()
        
        // Effectとしてタイマーを定義
        return state.isTimerActive
              ? Effect.timer(
                id: timerId(),
                every: 0.1,
                tolerance: .zero,
                on: environment.mainQueue
              )
              .map { _ in ClockAction.clockTicked }
              : Effect.cancel(id: timerId())
        
    // 時計の動作
    case .clockTicked:
        let now = Date()
        var hour = Calendar.current.component(.hour, from: now)
        let minute = Calendar.current.component(.minute, from: now)
        let second = Calendar.current.component(.second, from: now)
        
        if state.hourClock == .twelve {
            // 12時制の場合
            if hour >= 12 {
                state.ampm = .pm
                hour -= 12
            } else {
                state.ampm = .am
            }
        } else {
            state.ampm = nil
        }
        
        state.time = String(format: "%02d:%02d:%02d", hour, minute, second)
        return .none
    
    // 時制の切り替え
    case .hourClockToggled:
        switch state.hourClock {
        case .twelve:
            state.hourClock = .twentyFour
        case .twentyFour:
            state.hourClock = .twelve
        }
      return .none
    }
}

View

Storeをプロパティとして持ち、WithViewStore内でViewを定義することで、Storeを通してStateを参照したりActionを発行できる。

import SwiftUI
import ComposableArchitecture

struct ClockView: View {
    let store: Store<ClockState, ClockAction>
    
    let fontSizeLarge: CGFloat = 80
    let fontSizeSmall: CGFloat = 48
    
    var body: some View {
        // StoreをObservableなViewStoreに変換する構造体で、StateをもとにViewが更新される。
        WithViewStore(self.store) { viewStore in
            VStack(alignment: .leading) {
                // AMPM表示
                HStack(spacing: 4) {
                    Text("AM").foregroundColor(viewStore.state.ampm == .am ? .black : .gray)
                    Text("PM").foregroundColor(viewStore.state.ampm == .pm ? .black : .gray)
                }.onTapGesture {
                    // 時制切り替えのアクションを発行
                    viewStore.send(ClockAction.hourClockToggled)
                }

                // 時刻表示
                Text(viewStore.time)
                    .font(Font(UIFont.monospacedDigitSystemFont(ofSize: fontSizeLarge, weight: .light)))
            }
            .onAppear {
                // 時計スタートのアクションを発行
                viewStore.send(ClockAction.clockStarted)
            }
        }
    }
}

以上で画像のような簡単な時計アプリができ、AMPMをタップで12時制/24時制の切り替えができる。

まとめ

The Composable ArchitectureはMVPやMVVMなどのGUIアーキテクチャとは異なり、データの流れに注目し、単一方向データフローのFluxやReduxをもとに開発されたシステムアーキテクチャである。SwiftUI開発では、今まで以上にデータの流れやステータスの管理が大切になってくることから、SwiftUI時代のアーキテクチャとしてTCAが注目されている。

参考


この記事が気に入ったらサポートをしてみませんか?