[Xcode 11] MVVM with Combine in SwiftUI

[@akifumifukaya](https://twitter.com/akifumifukaya)です。
先日、発表した内容を記事として残しておこうと思います。
**※ 2019/10/21 にXcode 11に対応した内容に更新しました。**

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">MVVM with Combine 本日の発表資料です！ <a href="https://twitter.com/hashtag/wwdc19?src=hash&amp;ref_src=twsrc%5Etfw">#wwdc19</a><a href="https://t.co/JP8qyA06ju">https://t.co/JP8qyA06ju</a></p>&mdash; akifumi (@akifumifukaya) <a href="https://twitter.com/akifumifukaya/status/1139122642919731200?ref_src=twsrc%5Etfw">2019年6月13日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

スライドは以下にアップロードしています。
https://speakerdeck.com/akifumifukaya/mvvm-with-combine-in-swiftui

2019年8月5日に発表した資料はこちらです。
https://speakerdeck.com/akifumifukaya/mvvm-with-combine-ef82b6ce-8fae-4bb1-9643-ea613334c8f8

# What is Combine?
Combineとは、AppleがWWDC19で発表した新しいフレームワークのことです。

iOS 13.0, macOS 10.15, UIKit for Mac 13.0+, tvOS 13.0, watchOS 6.0+で使用することができます。
イベントを処理するオペレータを組み合わせることで、非同期イベントの処理を行うことができます。

フレームワークの詳細は、以下のドキュメントで見ることができます。
https://developer.apple.com/documentation/combine

# Combine Features
Combineの特徴として、以下の4つが挙げられています。

- Generic 
- Type safe
- Composition first
- Request driven

Generic, Type safe はSwiftの強みを引き継いでいます。
Composition firstは、チェーンのように処理をつなげて・組み立てながら宣言することができるので、コードが読みやすかったり、処理の流れがわかりやすくなっています。
Request drivenは、必要なタイミングで処理が行われるため、メモリ使用やパフォーマンスにも良い影響を与えます。

※ こちらの内容は、 [Introducing Combine](https://developer.apple.com/videos/play/wwdc2019/722/) から引用しています。

# Key Concepts
主要な概念として、以下の3つがあります。

- Publishers
- Subscribers
- Operators

※ こちらの内容は、 [Introducing Combine](https://developer.apple.com/videos/play/wwdc2019/722/) から引用しています。

# Publishers
- `Publishers`は時系列に連続した値を伝えることができる型を宣言できます
- 値やエラーがどのように生み出されるを定義します
- Value型、つまり`Struct`で定義されています
- `Subscriber`に登録することができます

※ こちらの内容は、 [Introducing Combine](https://developer.apple.com/videos/play/wwdc2019/722/) から引用しています。

# Subscribers
- Subscribersは`Publisher`からの入力を受け取ることができる型を宣言できます
- `Publisher`から値やcompletionを受け取ります
- Reference型、つまり`Class`で定義されています

※ こちらの内容は、 [Introducing Combine](https://developer.apple.com/videos/play/wwdc2019/722/) から引用しています。

# Operator
- オペレータは`Publisher`です
- 値が変化したときの振る舞いを表します
- `Publisher`をサブスクライブすることを**upstream**と呼びます
- `Subscriber`へ値を送ることを**downstream**と呼びます
- Value型、つまり`Struct`で定義されています

※ こちらの内容は、 [Introducing Combine](https://developer.apple.com/videos/play/wwdc2019/722/) から引用しています。

# How to actualize MVVM with Combine
では、どのようにCombineを使用してMVVMを実現するかを説明したいと思います。
https://github.com/akifumi/mvvm-with-combine-in-swiftui/blob/master/CombineSample/ContentView.swift

# What are requirements?
今回、実現したいことは以下です。

- `TextField`を追加し、テキストを入力できるようにする
- 入力されたテキストをバリデーションする
- ロジック部分を`ViewModel`として分け、`view`と`model`を分離する
- `view`と`ViewModel`をつなぎ合わせる
- `TextField`のコンテンツをtwo-wayバインディングする

# ContentViewにTextFieldを追加する

コードは以下のようになります。

```swift
struct ContentView : View {
    @State private var username: String = ""

    var body: some View {
        VStack {
            TextField("Placeholder", text: $username, onEditingChanged: { (changed) in
                print("onEditingChanged: \(changed)")
            }, onCommit: {
                print("onCommit")
            })
        }
        .padding(.horizontal)
    }
}
```

`VStack`で縦方向の`StackView`を作成し、その中に`TextField`を配置しています。
変数に`@State`を付けることによって、Viewと変数をtwo-wayバインディングで紐付けることができます。
また`$変数`のように`$`プレフィックスを付けることで、`Binding`オブジェクトを取得することができます。
ここから`ViewModel`に分割していきます。

# ViewModelを作成

次に`ViewModel`を作成します。

```swift
final class ContentViewModel : ObservableObject, Identifiable {
    @Published
    var username: String = ""
}
```

ViewModelを作成し、`ObservableObject`を適応します。
~~`ObservableObject`は`var objectWillChange: Self.ObjectWillChangePublisher { get }`を宣言することを要求しています。~~

~~```swift
var objectWillChange = PassthroughSubject<Void, Never>()
```~~
~~今回は単純な変更通知を送るために`PassthroughSubject`を用いて宣言しました。
また`username`という変数を定義し、`willSet`で`objectWillChange.send(())`を呼びます。~~
~~`willSet`が呼ばれると、SwiftUI側へ`View`の更新通知が行うようにしています。~~

**`@Published`を使用すると`objectWillChange`のパブリッシャーは`willSet`で自動的に呼ばれるようになりました。**

<blockquote class="twitter-tweet"><p lang="en" dir="ltr">In Beta 5 ObjectBinding is now defined in Combine as ObservableObject (the property wrapper is now <a href="https://twitter.com/ObservedObject?ref_src=twsrc%5Etfw">@ObservedObject</a>). <br>There is also a new property wrapper <a href="https://twitter.com/published?ref_src=twsrc%5Etfw">@Published</a> were we automatically synthesize the objectWillChange publisher and call it on willSet. <a href="https://t.co/IyXJZr3Ndf">pic.twitter.com/IyXJZr3Ndf</a></p>&mdash; Luca Bernardi (@luka_bernardi) <a href="https://twitter.com/luka_bernardi/status/1155944329363349504?ref_src=twsrc%5Etfw">July 29, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# Publisherを作成し、usernameをバリデーションする

```swift
final class ContentViewModel : ObservableObject, Identifiable {
    ︙
    @Published
    var username: String = ""
    private var validatedUsername: AnyPublisher<String?, Never> {
        return $username
            .debounce(for: 0.1, scheduler: RunLoop.main)
            .removeDuplicates()
            .flatMap { (username) -> AnyPublisher<String?, Never> in
               Future<String?, Never> { (promise) in
                    // FIXME: API request
                    if 1...10 ~= username.count {
                        promise(.success(username))
                    } else {
                        promise(.success(nil))
                    }
                }
                .eraseToAnyPublisher()
            }
            .eraseToAnyPublisher()
    }
}
```

`$username`で`username`の変更を受け取ることができます。
次に`username`にバリデーションを行うために、`var validatedUsername: AnyPublisher<String?, Never>`を定義しています。
今回はAPIの実装は行いませんでしたが、`Future`を用いることで非同期処理も行うことができます。
今回は`username`が有効であれば、`username`を返し、無効であれば`nil`を返すように実装しました。

# ContentViewModel.StatusText を作成
```swift
final class ContentViewModel : ObservableObject, Identifiable {
    ︙
    struct StatusText {
        let content: String
        let color: Color
    }
    @Published
    var status: StatusText = StatusText(content: "NG", color: .red)
}
```

バリデーションの結果を見やすいように、簡単なラベルを追加します。
`StatusText`はテキストとカラーも持つオブジェクトにしました。

# ViewModelにonApprear()を追加

`ContentView`が表示された際の処理を記述していきます。

```swift
final class ContentViewModel : ObservableObject, Identifiable {
    ︙
    private var cancellables: [AnyCancellable] = []
    private(set) lazy var onAppear: () -> Void = { [weak self] in
        guard let self = self else { return }
        self.validatedUsername
            .sink(receiveValue: { [weak self] (value) in
                if let value = value {
                    self?.username = value
                } else {
                    print("validatedUsername.receiveValue: Invalid username")
                }
            })
            .store(in: &self.cancellables)

        // Update StatusText
        self.validatedUsername
            .map { (value) -> StatusText in
                if let _ = value {
                    return StatusText(content: "OK", color: .green)
                } else {
                    return StatusText(content: "NG", color: .red)
                }
            }
            .sink(receiveValue: { [weak self] (value) in
                self?.status = value
            })
            .store(in: &self.cancellables)
    }
}
```

`username`が有効の場合、`username`を有効な値で書き換えています。

```swift
        self.validatedUsername
            .sink(receiveValue: { [weak self] (value) in
                if let value = value {
                    self?.username = value
                } else {
                    print("validatedUsername.receiveValue: Invalid username")
                }
            })
            .store(in: &self.cancellables)
```

`username`が有効の場合は<font color="Green">OK</font>、無効な場合は<font color="Red">NG</font>と表示する`StatusText`を作成し、`View`を更新するようにしています。

```swift
        // Update StatusText
        self.validatedUsername
            .map { (value) -> StatusText in
                if let _ = value {
                    return StatusText(content: "OK", color: .green)
                } else {
                    return StatusText(content: "NG", color: .red)
                }
            }
            .sink(receiveValue: { [weak self] (value) in
                self?.status = value
            })
            .store(in: &self.cancellables)
```

`sink()`関数で`AnyCancellable`が返ってくるので、`store`関数で開放されないように保持することができます。
これをしないと更新通知を受け取ることができません。

# ViewModelにonDisapprear()を追加

`ContentView`が非表示になった際の処理を記述していきます。

```swift
final class ContentViewModel : ObservableObject, Identifiable {
    ︙
    private var cancellables: [AnyCancellable] = []
    ︙
    private(set) lazy var onDisappear: () -> Void = { [weak self] in
        guard let self = self else { return }
        self.cancellables.forEach { $0.cancel() }
        self.cancellables = []
    }
}
```

`onApprear`で`cancellables`に保持していた`AnyCancellable`の配列をViewの非表示時に開放してあげます。

# ContentViewとContentViewModelを紐付ける

```swift
struct ContentView : View {
    @ObservedObject var viewModel: ContentViewModel

    var body: some View {
        VStack {
            HStack {
                Text($viewModel.status.wrappedValue.content)
                    .foregroundColor($viewModel.status.wrappedValue.color)
                Spacer()
            }
            TextField("Placeholder", text: $viewModel.username, onEditingChanged: { (changed) in
                print("onEditingChanged: \(changed)")
            }, onCommit: {
                print("onCommit")
            })
        }
        .padding(.horizontal)
        .onAppear(perform: viewModel.onAppear)
        .onDisappear(perform: viewModel.onDisappear)
    }
}
```

このように記述することで`ContentView`と`ContentViewModel`をバインディングしました。
ViewとModelをきれいに分離することができ、とても良いですね！！

# まとめ
- Combineの紹介を行いました
- SwiftUI, Combine & MVVM のサンプルコードを作成し、MVVMアーキテクチャをどのように実現するかを紹介しました
- SwiftUIのアーキテクチャのベストプラクティスはこれから生まれてくると思うのでみんなが考えましょう！
- Samples
    - SwiftUI: https://github.com/akifumi/mvvm-with-combine-in-swiftui
    - UIViewController: https://github.com/akifumi/mvvm-with-combine-in-uiviewcontroller

# 参考

- [Introducing Combine](https://developer.apple.com/videos/play/wwdc2019/722/)
- [Combine in Practice](https://developer.apple.com/videos/play/wwdc2019/721/)
- [Data Flow Through SwiftUI](https://developer.apple.com/videos/play/wwdc2019/226/)

# サンプルコード
- [MVVM with Combine in SwiftUI](https://github.com/akifumi/mvvm-with-combine-in-swiftui)
- [MVVM with Combine in UIViewController](https://github.com/akifumi/mvvm-with-combine-in-uiviewcontroller)