130
90

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

もっともシンプルな SwiftUI - MVVM

Last updated at Posted at 2021-07-16

#SwiftUI - MVVM が理解できます

「 MVVM って、よく聞くけど、なんかわかったようなわからないような。。」
「 流石にそろそろ SwiftUI いじっときたいけど、書く手順とかコードの配置とかややこしそう」
と思っているそこのあなた、 この記事を読めば、 SwiftUI - MVVM のデザインの骨子がわかります。

#MVVM とは?

MVVM はコードデザインのパターンで、
Model と View を分離する
ことに主眼をおいています。

Model はアプリが何をするかの実質内容
View はアプリをユーザーにどのように提示するかの方法

そして、両者の変更を

ViewModel が翻訳してつたえあう

ことにより、
Model はアプリの実質内容をわかりやすく一意に保つことができ、
View は Model の反映を遅滞なくユーザーに提示できます。

#MVVM は必須?

SwiftUI をつかってアプリをつくるために、 MVVM のパターンは必須ではありませんが、
MVVM をもちいることにより、
「アプリの内容変更をドバッと View にわたしてしまえば、 View がよろしく表現してくれる」宣言型の手法がとれ、スムースにアプリがかけるようです。
(ちなみに、「内容の更新ごとに View に『あれやってこれやって』と言う」のは命名型)

#一番シンプルな例で理解しよう

とはいえ、
「ViewModel ってけっきょくなにを書くねん」、「 SwiftUI って @ObservedObject とか @Published とかいろいろ新キャラがでてきてこわそう」
、と感じるのが人情だと思うので(ぼくがそうなので)、
シンプルなケーススタディーで SwiftUI と MVVM の組み合わせを書いてみました。

最低限の Model View ViewModel で MVVM のパターンをつくります。

ケーススタディーは、タップすると犬 ⇄ 猫が切り替わるスイッチです。

【画像:タップすると切り替わるシンプルなスイッチ】

スクリーンショット 2021-07-15 14.18.08.png タップ ⇄ スクリーンショット 2021-07-15 14.19.10.png

(わかりやすいように背景緑色にしてます)

#シンプルな MVVM

###Model を書く

このサンプルの Model 、つまりこのアプリケーションの実質は、犬と猫の切り替えです。

Model.swift
import Foundation // Model は SwiftUI をインポートしない

struct Model {
    
    enum Pet:String { // ケースは犬か猫か
        case 🐶
        case 🐱
    }

    var pet: Pet = .🐶 // 初期値は犬
    
    mutating func switchPet() { // 犬と猫を切り替える関数
        if pet == .🐶 {
            pet = .🐱
        } else {
            pet = .🐶
        }
    }
    
}

Model は SwiftUI をインポートしません。 UI から独立したアプリの実質だからです。

このアプリは犬なのか、猫なのか、の切り替えが実質なので、
Model は、犬か猫かの pet という変数と、 犬と猫を切り替える switchPet という関数でできています。
(struct が自身を変更するには mutating func をもちいます)

我々のアプリの Model はこれだけです。

###View を書く

View は Model の pet を Text View にして表示します。
また、 Text View がタップされたら、 Model の pet をスイッチします。

ContentView.swift
import SwiftUI

struct ContentView: View {
    
    var body: some View {
        Text("ここに Model の pet を反映する")
            .padding()
            .onTapGesture {
                // ここで model の pet をスイッチする
        }
    }

}

我々の View は
ユーザーに対する Model 内容の表示と、
ユーザーのタップを受け入れる役割を持っています。

MVVM のパターンを無視すれば、
ここで Model を View で直接保持することも可能です。

ダイレクトにModelをもつContentView.swift
import SwiftUI

struct ContentView: View {
    @State var model = Model()
     // @State をつけることで、 View の状態に関する値を変更、即時反映できる

    var body: some View {
        Text(model.pet.rawValue)
            .padding()
            .onTapGesture {
                model.switchPet()
        }
    }

}

もっといえば、pet 変数と切り替え関数を View で持つことも、もちろん可能です。

ダイレクトにpetとswitchPetをもつContentView.swift
import SwiftUI

struct ContentView: View {

    enum Pet:String {
        case 🐶
        case 🐱
    }

    @State var pet: Pet = .🐶
     // @State をつけることで、 View の状態に関する値を変更、即時反映できる
    
    mutating func switchPet() {
        if pet == .🐶 {
            pet = .🐱
        } else {
            pet = .🐶
        }
    }

    var body: some View {
        Text(pet.rawValue)
            .padding()
            .onTapGesture {
                switchPet()
        }
    }

}

このペットという変数が View の一時的な状態をあらわすだけなら、これでいいのかもしれません。

しかしそうすると、たとえば、いくつも View があったときに Model の状態を一意に保つのが大変になったりします。
それをやらないのが MVVM です。

###ViewModel を書く

ViewModel の役割は、 View と Model のコミュニケーションの通訳です。
View からユーザーのタップがあったときに Model に伝え
Model の状態を View に伝えることです。

ViewModel.swift
import Foundation
import SwiftUI

class ViewModel {
    var model:Model = Model() // Model をもつ
    
    var pet: String { 
        return model.pet.rawValue // Model の pet を View が必要とする String にして返す
    }
    
    func switchPet() {
        model.switchPet() // Model の switchPet を呼ぶ
    }
}

###View から ViewModel にアクセスする

View から ViewModel にユーザーのタップを伝え、
ViewModel から Model の切り替え関数を呼び、
ViewModel から View は Model の pet の値の変更を取得します。

ContentView.swift
import SwiftUI

struct ContentView: View {
    var viewModel = ViewModel()
    
    var body: some View {
        ZStack {
            Text(viewModel.pet)
                .padding()
                .onTapGesture {
                    viewModel.switchPet()
                }
        }
    }
}

これを動かしてみると、 UI が変わりません

確認のために、 Model のスイッチペットにプリントを入れてみると、

Model.swift
    mutating func switchPet() {
        if pet == .🐶 {
            pet = .🐱
        } else {
            pet = .🐶
        }
        print(pet)
    }

🐱
🐶
🐱
🐶

Model の pet は切り替わっていますが、 UI は更新されていません。
先ほどの情報遷移のフローで言うと、

View から ViewModel にユーザーのタップを伝え、(できた)
ViewModel から Model の切り替え関数を呼び、(できた)
ViewModel から View は Model の pet の値の変更を取得します。(ここが届いていない)

MVVM では、 ViewModel は全体に向けて Model の変更をパブリッシュ(公開)し、 View は自分の知りたい情報を任意に購読(subscribe)する、というかたちで Model の更新情報を取得します。

ここで、SwiftUI のプロパティ・ラッパーが登場します。

###ViewModel が変更を公開し、 View が購読する

ViewModel.swift
import Foundation
import SwiftUI

class ViewModel:ObservableObject { // @ObservableObject をつける
    @Published var model:Model = Model() // @Published をつける
    
    var pet: String {
        return model.pet.rawValue
    }
    
    func switchPet() {
        model.switchPet()
    }
}

@ObservableObject (観察可能なオブジェクト)を継承することで、 ViewModel は観察対象になることができ、アプリ全体(の観察意思がある対象)に向けて情報を発信できるようになります。

@Published をつけることで、 この Model に変更があったとき即座に、 ViewModel (@ObservableObject) は全体にパブリッシュ(公開)できます。

そして、 View 側でこの変更のパブリッシュを購読します。

ContentView.swift
import SwiftUI

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel() // @ObservedObject をつける
    
    var body: some View {
        ZStack {
            Text(viewModel.pet)
                .padding()
                .onTapGesture {
                    viewModel.switchPet()
                }
        }
    }
}

@ObservedObject (観察されるオブジェクト)を viewModel 変数につけることで、 @ObservableObject である ViewModel の公開する変更があったときに、 View は即時に body var から関連する UI を変更できます。

これで、 「ViewModel から View は Model の pet の値を取得する」部分ができあがり、タップによって UI も更新されるようになりました。

【画像:タップで更新される犬猫】
Jul-16-2021 10-45-42.gif

これがもっともシンプルな MVVM です。
もっと色々あるんでしょうが、とりあえずのパターンの基本要素は入っていると思います。

🐣


フリーランスエンジニアです。
お仕事のご相談こちらまで
rockyshikoku@gmail.com

Core MLを使ったアプリを作っています。
機械学習関連の情報を発信しています。

Twitter
Medium

130
90
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
130
90

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?