🍣

MVVMでUIKitとSwiftUIを共存する最小コード

2024/02/14に公開

想定読者: UIKitベースのプロジェクトから徐々にSwiftUIベースに移行したい人

自分の現在の現場で経験したMVVMを使ってUIKitとSwiftUIを併存させた経験をもとにした記事になります。同じような作りを保つことで既存のUIKitベースのプロジェクトからSwiftUIベースにスムーズに移行が出来ます。
また@StateObjectObservableObjectなどの用語の細かい説明はしません。このコードを読んでみて気になったら公式や別の記事を参考にしてください。

SwiftUIにMVVMは必要なのか

このテーマについては色々な意見がありますが、私の考えでは不要であると考えてます。とくにTCAを使って構築していくのであればさらに不要です。
ではなぜこの記事を書いているのかと言うと、多くのiOSプロジェクトではUIKitベースで作られているのが大半だからです。また残念ながら古い世代のOSもサポートを考えると、まだUIKitベースで作った方がUIが安定するケースも多いです。さらに例えばUIKit+MVVMベースのプロジェクトでSwiftUIに切り替えかつMVVMをやめるとなると、大きく2つの変更により現場が混乱する可能性もあるので中間案としてMVVMの構造を残しておくというのは一つの解決策になります。

iOS17以上

サンプルコードはCombineの機能を一部利用していますが、iOS17以上のみをサポートするのでしたら、Observationを使う方がいいと思います。ただ今回対象にしている読者のように古いOSもサポートする必要がある場合はCombineを使用します。具体的にはViewControllerとViewModelのBindingにCombineを使います。

ModelとViewModelを定義する

struct Article: Identifiable, Hashable {
    let id: Int
    let title: String

    func hash(into hasher: inout Hasher) {
        hasher.combine(id)
        hasher.combine(title)
    }
}

final class ArticleListViewModel: ObservableObject {
    @Published var articles: [Article] = []
}

まずはUIに依存がない、ModelとViewModelを見ていきます。ArticleIdentifiable, Hashableを準拠することでこの後説明するSwiftUIでのリスト表示をやりやすくするためです。またViewModelにはObservableObjectを準拠することでこの後説明するStateObjectを使用するためです。

articlesを購読できるように@Publishedを指定しています。

ViewController, Viewの定義

import UIKit
import Combine

final class ArticleListViewController: UIViewController {
    private var viewModel: ArticleListViewModel = .init()
    private var cancellables: Set<AnyCancellable> = []

    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            collectionView.dataSourse = self
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()

        viewModel.$articles
            .removeDuplicates()
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: { [weak self] _ in
                self?.collectionView.reloadData()
            }).store(in: &cancellables)
    }
}

// MARK: - UICollectionViewDataSource

extension ArticleListViewController: UICollectionViewDataSource {
    // 省略
}

まずはViewControllerから見ていきます。viewDidLoadのタイミングでarticlesの値を監視しています。そしてarticlesの値が変化したらreloadData()が実行されるようになっています。シンプルですね。

import SwiftUI

struct ArticleListView: View {
    @StateObject private var viewModel: ArticleListViewModel = .init()

    var body: some View {
        List {
            ForEach($viewModel.articles, id: \.self) { $article in
                Text(article.title)
            }
        }
    }
}

SwiftUIのこちらもシンプルでarticlesの値が変化したタイミングで画面が更新される作りになっています。articlesを監視するには@StateObjectを使って宣言する必要があります。上の方にも書いたのですが@StateObjectを使用するにはObservableObjectを準拠が必要です。

まとめ

プロジェクトによってはSwiftUIを全てUIKitに移行するにはかなり労力がかかるが、出来るだけ同じような作りにすること移行しやすい作りにするのは大切です。

Discussion