MVVMでUIKitとSwiftUIを共存する最小コード
想定読者: UIKitベースのプロジェクトから徐々にSwiftUIベースに移行したい人
自分の現在の現場で経験したMVVMを使ってUIKitとSwiftUIを併存させた経験をもとにした記事になります。同じような作りを保つことで既存のUIKitベースのプロジェクトからSwiftUIベースにスムーズに移行が出来ます。
また@StateObject
やObservableObject
などの用語の細かい説明はしません。このコードを読んでみて気になったら公式や別の記事を参考にしてください。
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を見ていきます。Article
にIdentifiable
, 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