🦋
SwiftUIでMVVMやるシンプルなパターン3つ
1. ViewModelをViewでバケツリレー
メリット 簡単
デメリット 小孫Viewの使い回しができない(ViewがViewModelに依存してしまう)
項目 | 特記事項 |
---|---|
Model | 特になし |
ViewModel | - ObservableObject プロトコルに準拠する- modelに @Published アノテーションをつける |
View | - 親ViewのviewModelに@StateObject アノテーションをつける- 子孫ViewのviewModelには @ObservedObject アノテーションをつける |
App
@main
struct SampleApp: App {
var body: some Scene {
WindowGroup {
SampleView(viewModel: SampleViewModel(model: SampleModel()))
}
}
}
Model
struct SampleModel {
var count: Int = 0
var stars: String = "★"
mutating func changeStarsLength(_ len: Int) {
self.stars = [String](repeating: "★", count: len).joined()
}
}
ViewModel
@MainActor
class SampleViewModel: ObservableObject {
@Published var model: SampleModel
init(model: SampleModel) {
self.model = model
}
var count: Int {
get {
return model.count
}
set {
model.count = newValue
}
}
var stars: String {
return model.stars
}
func changeStarsLength(_ len: Int) {
model.changeStarsLength(len)
}
}
View
struct SampleView: View {
@StateObject var viewModel: SampleViewModel
var body: some View {
VStack(spacing: 8) {
Text("\(viewModel.count)")
Button("Count Up") {
viewModel.count += 1
}
SampleSubView(viewModel: viewModel)
}
}
}
struct SampleSubView: View {
@ObservedObject var viewModel: SampleViewModel
init(viewModel: SampleViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack(spacing: 8) {
Text(viewModel.stars)
Button("Change Stars Length") {
viewModel.changeStarsLength(Int.random(in: 1 ..< 10))
}
}
}
}
2. プロパティはBinding、メソッドはクロージャーで保持
メリット 小孫Viewが使い回し可能になる
デメリット とくになし?
項目 | 特記事項 |
---|---|
Model | 特になし |
ViewModel | - ObservableObject プロトコルに準拠する- modelに @Published アノテーションをつける- 子ViewでBindingしたいComputed Propertyにはsetterを定義する |
View | - 親ViewのviewModelに@StateObject アノテーションをつける- 小孫ViewでViewModelのメソッドを叩く時はクロージャーを保持するようにする |
パターン1からModelには変化なし
ViewModel
@MainActor
class SampleViewModel: ObservableObject {
@Published var model: SampleModel
init(model: SampleModel) {
self.model = model
}
var count: Int {
get {
return model.count
}
set {
model.count = newValue
}
}
// 子ViewでBindingするためにはsetterが必要
var stars: String {
get {
return model.stars
}
set {
model.stars = newValue
}
}
func changeStarsLength(_ len: Int) {
model.changeStarsLength(len)
}
}
View
struct SampleView: View {
@StateObject var viewModel: SampleViewModel
var body: some View {
VStack(spacing: 8) {
Text("\(viewModel.count)")
Button("Count Up") {
viewModel.count += 1
}
// $をつけて渡す
SampleSubView(stars: $viewModel.stars) { len in
// クロージャーで処理を渡す
viewModel.changeStarsLength(len)
}
}
}
}
struct SampleSubView: View {
@Binding private var stars: String
private var handler: (Int) -> Void
init(stars: Binding<String>, action handler: @escaping (Int) -> Void) {
self._stars = stars
self.handler = handler // クロージャーを保持
}
var body: some View {
VStack(spacing: 8) {
Text(stars)
Button("Change Stars Length") {
handler(Int.random(in: 1 ..< 10))
}
}
}
}
3. EnvironmentObjectでシングルトンなViewModelにアクセス
メリット バケツリレーしなくていい
デメリット 小孫Viewの使い回しができない(ViewがViewModelに依存してしまう)
項目 | 特記事項 |
---|---|
App | - .environmentObject() でViewにViewModelを渡しておく |
Model | 特になし |
ViewModel | - ObservableObject プロトコルに準拠する- modelに @Published アノテーションをつける |
View | - ViewModelが必要な各Viewで@EnvironmentObject アノテーションをつけてviewModelを定義 |
App
@main
struct Sample3App: App {
var body: some Scene {
WindowGroup {
SampleView()
.environmentObject(SampleViewModel(model: SampleModel()))
}
}
}
パターン1からModel、ViewModelには変化なし
View
struct SampleView: View {
@EnvironmentObject var viewModel: SampleViewModel
var body: some View {
VStack(spacing: 8) {
Text("\(viewModel.count)")
Button("Count Up") {
viewModel.count += 1
}
SampleSubView()
}
}
}
struct SampleSubView: View {
@EnvironmentObject var viewModel: SampleViewModel
var body: some View {
VStack(spacing: 8) {
Text(viewModel.stars)
Button("Change Stars Length") {
viewModel.changeStarsLength(Int.random(in: 1 ..< 10))
}
}
}
}
番外:ViewModelとModelのやりとりをCombineでやる
Computed PropertyではなくCombineのPublisherを使ってViewModelとModelのやりとりをやる。
Model
// class にしてプロパティに@Publishedアノテーションをつける
final class SampleModel {
@Published var count: Int = 0
@Published var stars: String = "★"
func changeStarsLength(_ len: Int) {
self.stars = [String](repeating: "★", count: len).joined()
}
}
ViewModelの例1
@MainActor
class SampleViewModel: ObservableObject {
@Published var count: Int
@Published var stars: String
private var model: SampleModel
init(model: SampleModel) {
self.model = model
// 初期値を渡す
self.count = model.count
self.stars = model.stars
// modelの値が変わったらviewModelに反映させる
self.model.$count.assign(to: &$count)
self.model.$stars.assign(to: &$stars)
}
func changeStarsLength(_ len: Int) {
model.changeStarsLength(len)
}
}
assign
ではなくsink
を使う手もある。
ViewModelの例2
@MainActor
class SampleViewModel: ObservableObject {
@Published var count: Int
@Published var stars: String
private var model: SampleModel
private var cancellables = Set<AnyCancellable>()
init(model: SampleModel) {
self.model = model
self.count = model.count
self.stars = model.stars
self.model.$count
.sink(receiveValue: { count in
self.count = count
})
.store(in: &cancellables)
self.model.$stars
.sink(receiveValue: { stars in
self.stars = stars
}).store(in: &cancellables)
}
func changeStarsLength(_ len: Int) {
model.changeStarsLength(len)
}
}
Discussion
GitHub にソースを置きました。
以下の記事も参考までに
SwiftUI版のもあります。