SwiftUIでアプリを構築していてプレビュー機能をフル活用しないことにはSwiftUI使ってる旨味を最大限に引き出せませんよね。
このWWDC動画でのプレビュー機能の活用方法がとってもわかりやすく実用的だったので、要点だけまとめて記事にします。
シンプルデータとリッチデータ
動画で語られているのが、アプリで取り扱うデータは主に2つに分類できると言及されています。
- リッチデータ ... CoreData, RealmやCloudKitなどの情報やサーバ側にしか無いデータなど
- シンプルデータ ... Stringなどのプリミティブなデータ型、構造体など
シンプルデータは生成や取得が容易でリッチデータは生成や取得へのレイヤーが深かったり手続きが面倒なものといったところでしょうか。
動画ではSwiftUIのView層は極力シンプルデータで構築しましょうと言っています。
仮にRealmやCloudKitのデータをViewで表示する場合であったとしてもリッチデータをそのままバインドするのではなくprotocolなどで抽象化し、シンプルデータとしてView側で表示するのを推奨しています。
リッチデータはテストやプレビューがしにくい
RealmやiCloudなどのリッチデータというのは生成がユーザ操作を経ないと出来ない場合や、ビューに必要ではない情報を多分に含んでいるケースなどが多く、テストやプレビューする時にはとても難儀です。
Realm上のデータをリストで表示
みたいなユースケースを抽象化せずに実装していくとプレビューしにくいのは想像に難くないと思います。
##プレビューしずらい設計
import Foundation
import SwiftUI
import RealmSwift
struct TodoView: View {
@ObservedObject var viewModel: TodoViewModel
var body: some View {
List(self.viewModel.list, id: \.id) { todo in
HStack {
Image(systemName: todo.isComplete ? "checkmark.square" : "square")
.foregroundColor(todo.isComplete ? .green : .secondary)
Text(todo.title)
}
.padding(6)
}
}
}
struct ContentView_Previews: PreviewProvider {
static let viewModel: TodoViewModel = TodoViewModel()
static var previews: some View {
TodoView(viewModel: viewModel)
}
}
// MARK: viewModel
class TodoViewModel: ObservableObject {
@Published var list: [TodoEntity]
private var dataSource: Realm
init() {
self.dataSource = try! Realm()
self.list = self.dataSource.objects(TodoEntity.self).map { $0 }
}
}
class TodoEntity: Object, Identifiable {
@objc dynamic var id: String = UUID().uuidString
@objc dynamic var title: String = ""
@objc dynamic var isComplete: Bool = false
}
TodoリストをRealmで表示するケースでの実装パターン
このままだとPreviewProviderで空データのプレビューが表示されるだけでデータがある場合のプレビューが出来ません。
抽象化しておらず、Viewが詳細な実装に依存しているため、追加処理を実装するまでデータの一覧での確認するのが辛く、あまりよろしい設計とは言えません。
今回のパターンは追加処理も簡単でプレビューしなくてもだいたいの画面はイメージできますが、より複雑なパターンや、RealmからCloudKitの載せ替えが発生した場合や、データをサーバーから取得する仕様に変更になった場合にサーバの実装が出来るまで待ち時間が発生してしまいます。
フロントエンドエンジニアとしてそれは由々しき問題なので、ビューからデータの発生源の関心を取り除きましょう。
##ジェネリクスを適用しモックに差し替えやすく
import Foundation
import SwiftUI
struct TodoView<T: TodoViewModelProtocol>: View {
@ObservedObject var viewModel: T
var body: some View {
List(self.viewModel.list, id: \.id) { todo in
HStack {
Image(systemName: todo.isComplete ? "checkmark.square" : "square")
.foregroundColor(todo.isComplete ? .green : .secondary)
Text(todo.title)
}
.padding(6)
}
}
}
struct ContentView_Previews: PreviewProvider {
class TodoViewModelMock: TodoViewModelProtocol {
@Published var list: [TodoEntity] = [TodoEntity]()
init() {
self.list = [
TodoEntity(title: "first task", isComplete: true),
TodoEntity(title: "second task", isComplete: false),
TodoEntity(title: "third task", isComplete: true),
]
}
}
static var previews: some View {
TodoView<TodoViewModelMock>(viewModel: TodoViewModelMock())
}
}
// MARK: viewModel
protocol TodoViewModelProtocol: ObservableObject {
associatedtype ListData: TodoEntityProtocol
var list: [ListData] { get set }
}
protocol TodoEntityProtocol {
var id: String { get set }
var title: String { get set }
var isComplete: Bool { get set }
}
class TodoEntity: Object, Identifiable, TodoEntityProtocol {
@objc dynamic var id: String = UUID().uuidString
@objc dynamic var title: String = ""
@objc dynamic var isComplete: Bool = false
convenience init(title: String, isComplete: Bool) {
self.init()
self.title = title
self.isComplete = isComplete
}
}
TodoViewModelProtocol
とTodoEntityProtocol
を新たに定義し、抽象化
SwiftUIのView層はProtocolのみ知っている状態にし、ジェネリクスを用いて差し替え容易に
struct TodoView<T: TodoViewModelProtocol>: View {
@ObservedObject var viewModel: T
PreviewProvider
にモックデータ用のクラスを差し込めば実際のデータ状態に依存されずにプレビュー可能に。
モックデータ用のクラスはシンプルデータを自前で用意するだけで良くなりました。
モッククラスを実行ファイルに同梱したくない場合はPreview Content
フォルダに含めれば、製品版に不要なソースがバンドルされることもないので、とても有能です。
##SwiftUIのプレビューはデータ設計にも強力なツール
SwiftUIのプレビューは魅力的で、簡易なビューならすぐプレビューできますが、複雑なユースケースが絡んだ場合に安直に実装してしまうと途端にプレビューしにくくなってしまいます。
これをデメリットに感じてしまう人もいるかもしれませんが、UIフレームワークが実装者に設計を意識させる作り方になっているのだと感じました。
抽象化のメリットを具体例で説明する時に毎回いい例を挙げれずに困っていましたが、今回のサンプルはいい例になるなと思い記事にしました。
UIKitは工夫しないとファットになってしまうアーキテクチャでしたが、SwiftUIは初期構想から実装者を良い設計に導くように作られているんだと思いました。