【SwiftUI】Combineを使った入力フォームのバリデーションチェック

# Combineを使ってTextFieldの入力のバリデーションチェック

入力フォームのバリデーションチェックを、Combineで実行します。

## 開発環境

- macOS 11
- Xcode 12 beta

## View側の実装

全体のソースコード。

```swift

struct CreateUserView: View {
    
    @StateObject var vm = CreateUserViewModel()
    
    var body: some View {
        NavigationView {
            Form {
                TextField("Username", text: $vm.username)
                TextField("Password", text: $vm.password)
                TextField("Password Again", text: $vm.passwordAgain)
                Button(action: {}, label: {
                    Text("Submit")
                })
                .disabled(!vm.readyToCreate)
                
                Section(header: Text("DEBUG")) {
                    Text(vm.readyToCreate ? "Ready to create" : "Waiting input")
                }
            }
            .navigationTitle("Create User")
        }
    }
}
```

`Form`でリストを構築し、ユーザー名の`TextField`と、パスワードと二回目パスワードの`SecureField`を入れる。

入力された情報は、ViewModelに持たせて、そこでバリデーションチェックを行う。
`readyToCreate`は、一回目パスワードと二回目パスワードが一致する、かつユーザー名の空ではないときに`true`になる。

Submitボタンは、バリデーションチェック結果の`readyToCreate`が`True`の場合のみ活性化になる。

デバッグのため、下に`readyToCreate`の値を表示する。

**※Xcode 12 RC/iOS14.2 でSecureFieldがバグっていて上手く表示されないこともある。その時はTextFieldに変えてください**


## ViewModel側の実装

全体のソースコード。

```swift

class CreateUserViewModel: ObservableObject {
    @Published var username = ""
    @Published var password = ""
    @Published var passwordAgain = ""
    @Published var readyToCreate = false
    
    private var anyCancellable = Set<AnyCancellable>()
    
    init() {
        $username
            .combineLatest($password, $passwordAgain)
            .map {
                let username = $0.0
                let password = $0.1
                let passwordAgain = $0.2
                
                guard username.count > 0, password.count > 0, passwordAgain.count > 0 else { return false }
                guard password == passwordAgain else { return false }
                return true
            }
            .assign(to: \.readyToCreate, on: self)
            .store(in: &anyCancellable)
    }
}
```
#### ①ストリームを合成する
```swift

$username
    .combineLatest($password, $passwordAgain)
```

combineLatestを使って`$username`、`$password`と`$passwordAgain`の3つのストリームを結合させる。
どちらかが新しい値が出力されたとき、3ストリームの一番新しい値がタプルにとして再出力される。
combineLatestの詳しい説明は、[こちら](https://qiita.com/shiz/items/cff44fe801243b8a5496#combinelatest)を参考ください。

#### ②バリデーションチェック
```swift

.map {
    let username = $0.0
    let password = $0.1
    let passwordAgain = $0.2

    guard username.count > 0, password.count > 0, passwordAgain.count > 0 else { return false }
    guard password == passwordAgain else { return false }
    return true
}
```
mapを使って`username`、`password`と`passwordAgain`の文字数をチェックする。
`$password`と`$passwordAgain`が一致するのをチェックする。
両方チェックOKのみ`true`を出力する。それ以外は`false`を出力する。

#### ③画面更新
```swift

.assign(to: \.readyToCreate, on: self)
```
mapで出力されたパスワードチェックの結果を`readyToCreate`に設置する。
`readyToCreate`が更新されると、View側も更新される。

#### ④AnyCancellable
```swift

.store(in: &anyCancellable)
```
`AnyCancellable`をストアする。

## まとめ
SwiftUIとCombineを使えば、簡単にバリデーションチェックを作れます。
両方ともiOS13以降しか使えないのがネックですが、今後は大きく期待できると思います。
（みんなFlutterで盛り上がっているみたいですが。。。）