💪

SwiftUIをClean Architectureで実装してみました

2025/01/22に公開

株式会社バニッシュ・スタンダードでiOS向けアプリの開発をやっている広瀬です。
弊社では順次SwiftUIに移行中なんですが、SwiftUIを利用するにあたってアーキテクチャに何を採用するのかは議題にあがりがちだと思います。
多くはMVVMを使っていることだと思いますが、弊社では一部のiOS向けアプリでClean Architectureを採用して開発を進めています。
今回はなぜClean Architectureを採用するに至ったのか、またどの様に利用しているかを話していこうと思います。

解決したい問題

なぜClean Architectureを採用するに至ったかですが、複数画面に渡って値の受け渡しをしたい場合にViewModelを経由しての受け渡しが難しくなるケースがあったためなのと、SwiftUIのPreviewを利用する場合にロジックとpropertyを切り離したほうが容易なのではと考えたためでもあります。

ではさっそくサンプルをみていきます。(適当に作っているので実行はできません)

クラス構成

クラス図

クラス構成は以上の様にClean Architecture +αの形になっています。
(UseCase以下は略)
以下、presentation層の実装を紹介します。

実装

TopView
struct TopView: View {
    @ObservedObject private var state: TopViewState
    private var action: TopViewAction

    init(state: TopViewState, action: TopViewAction) {
        self.state = state
        self.action = action
    }

    var body: some View {
        VStack {
            if state.isLogin {
                HomeView()
            } else {
                LoginView()
            }
        }
        .onApear {
            action.onApear()
        }
    }
}

#Preview {
    TopView(state: TopViewStateMock(), action: TopViewActionMock())
}
TopViewState
final class TopViewState: ObservableObject {
    @Published var isLogin = false
    @Published var showError = false

    init() {}
}
TopViewAction
struct TopViewAction {
    let delegate: TopDelegateAction

    init(state: TopViewState) {
        self.delegate = TopPresenter(useCase: LoginUseCase(), state: state)
    }

    func onApear() {
        delegate.onApear()
    }
}
TopPresenter
protocol TopDelegateAction {
    func onApear()
}

final class TopPresenter {
    private let useCase: LoginUseCase

    init(useCase: LoginUseCase, state: TopViewState) {
        self.useCase = useCase
        self.state = state
    }
}

extension TopPresenter: TopDelegateAction {
    func onApear() {
        Task {
            do {
                state.isLogin = try await useCase.login()
            } catch {
                state.showError = true
            }
        }
    }
}

ご覧の様にMVVMと比較した際にViewModelが担当していたoutputはViewStateに、inputがViewAction、ロジックはPresenterが受け持っています。

おわりに

今回紹介しましたアーキテクチャを採用することで、Previewを簡潔に書ける様になったのと、ViewModelの責務を分割したことで、Previewする際に肥大化するViewModelを参照する必要がなくなったためビルドプロセスを軽くすることもできました。
また、ViewStateをStoreに変更するなどした場合など、グローバルにやりとりをすることもやりやすくなりました。

まだまだこのアーキテクチャは完璧ではなくて色んなケースで問題が発生していて試行錯誤の段階ですが、以上、弊社iOSアプリで採用しているアーキテクチャの簡単な紹介でした。

次回移行アップデートあればまた続きを執筆したいと思います。

株式会社バニッシュ・スタンダード

Discussion