はじめに
SwiftUIでも少しでも複雑な画面になってくるとCombineのObservableObjectの入力と出力を分けたいときがあるわけです。ぱっと見で何が入力で出力なのか整理したいし、他の人にも整理してコードを書いてほしい。
RxSwiftのときはRxSwift研究読本3 ViewModel設計パターン入門のようにRxCocoaがあるので工夫ができるわけだけど、SwiftUI+Combineではどうしていくと整理されるんだろうというわけで、この記事は簡単なサンプルを作って複数のやり方を示してみます。
かんたんな画面例
2つのTextFieldがあってその2つの文字が同じ場合にButtonを押せるような画面を例とします。
ボタンを押したときにTextFieldの値を見張るのではなく、TextFieldの文字をリアルタイムで見張りたいのです。
どうやるか
雑に3つくらい考えられると思います
- 何も考えずコメントでinputやoutputとか書いてみる例
- input/outputを型にしてみる例
- PublishedをやめてSubjectを使う例
何も考えずコメントでinput/outputとか書いてみる例
まず、もう何も考えずにやれるようにやっていく例。こうやってしまうと、@Published
が増えてinputかoutputか分かりづらい。
import Combine
// 何も考えずコメントでinput/outputとか書いてみる
class ObservableObject1: ObservableObject {
// input
@Published var password = ""
@Published var repeatedPassword = ""
// output
@Published var validatedPassword = false
private var cancellableSet: Set<AnyCancellable> = []
init() {
Publishers.CombineLatest($password, $repeatedPassword)
.map { $0 == $1 }
.eraseToAnyPublisher()
.assign(to: \.validatedPassword, on: self)
.store(in: &cancellableSet)
}
}
SwiftUI側はこんな感じ
import SwiftUI
struct ContentView: View {
@ObservedObject var object = ObservableObject1()
var body: some View {
VStack {
TextField("パスワード", text: $object.password)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("パスワード繰り返して!", text: $object.repeatedPassword)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
print("押されたよ")
}) {
Text("パスワードが一緒だと押せるよ!")
}
.disabled(!object.validatedPassword)
}
}
}
- 長所
- 最初は何も考えないで済む
- 短所
- 数が増えると整理されてなさがきつい
- inputは外から読まれる
- input用に
@Published
することで2-way Bindingしてる- ObservableObject側で変更しないわけで、
@Published
の意味がない-
@Published
はViewを更新させる- 参考: SwiftUIガイドブック
-
- ObservableObject側で変更しないわけで、
input/outputを型にしてみる例
これをRxSwift使ってたときのようにちょっと工夫してみる。Input
とOutput
型作ってみる。
import Combine
// InputとOutputをなるべく分けたいので型にした
class ObservableObject2: ObservableObject {
class Input {
@Published var password = ""
@Published var repeatedPassword = ""
}
class Output {
@Published var validatedPassword = false
}
// input
var input: Input
// output
var output: Output
private var cancellableSet: Set<AnyCancellable> = []
init(input: Input = Input(), output: Output = Output()) {
self.input = input
self.output = output
Publishers.CombineLatest(self.input.$password, self.input.$repeatedPassword)
.map { $0 == $1 }
.eraseToAnyPublisher()
.assign(to: \.validatedPassword, on: self.output)
.store(in: &cancellableSet)
}
}
SwiftUI側は次のような感じ
import SwiftUI
struct ContentView2: View {
@ObservedObject var object = ObservableObject2()
var body: some View {
VStack {
TextField("パスワード", text: $object.input.password)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("パスワード繰り返して!", text: $object.input.repeatedPassword)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
print("押されたよ")
}) {
Text("パスワードが一緒だと押せるよ!")
}
.disabled(!object.output.validatedPassword)
}
}
}
- 長所
- 型で整理された
- 短所
- 単にまとめただけなのでInputの
@Published
の意味がさらによくわからない感じになる - inputは外から読まれる
- 単にまとめただけなのでInputの
PublishedをやめてSubjectを使う例
とにかくinput側の@Published
が嫌なのかもしれない。プロパティが更新されたらSubjectを呼び出すようにする。
import Combine
// inputからPublishedをなくせばいい
class ObservableObject3: ObservableObject {
private let passwordSubject = PassthroughSubject<String, Never>()
private let repeatedPasswordSubject = PassthroughSubject<String, Never>()
// MARK: - input
var password = "" {
didSet {
passwordSubject.send(password)
}
}
var repeatedPassword = "" {
didSet {
repeatedPasswordSubject.send(repeatedPassword)
}
}
// MARK: - output
@Published var validatedPassword = false
// MARK: -
private var cancellableSet: Set<AnyCancellable> = []
init() {
Publishers.CombineLatest(passwordSubject, repeatedPasswordSubject)
.map { $0 == $1 }
.eraseToAnyPublisher()
.assign(to: \.validatedPassword, on: self)
.store(in: &cancellableSet)
}
}
SwiftUI側はこんな感じ。最初の例と同じ。
import SwiftUI
struct ContentView3: View {
@ObservedObject var object = ObservableObject3()
var body: some View {
VStack {
TextField("パスワード", text: $object.password)
.textFieldStyle(RoundedBorderTextFieldStyle())
TextField("パスワード繰り返して!", text: $object.repeatedPassword)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
print("押されたよ")
}) {
Text("パスワードが一緒だと押せるよ!")
}
.disabled(!object.validatedPassword)
}
}
}
- 長所
-
@Published
が付いているのは出力だけ
-
- 短所
- 外部からinputは読まれる
まとめ
- 一番最初の何も考えない例から、最後の
@Published
をやめてSubjectを使う例に移行するのはUI側を変えないので楽- 2-way Bindingしないなら出力にのみ
@Published
が付いているほうがコードを把握しやすい - しかし結局inputは外部から読まれる(読んで何が困るということはないけども)
- 2-way Bindingしないなら出力にのみ
- ちなみにどうあると嬉しいのか
- プロパティだとinputが外部から読まれる仕組みにならざるを得ない
- TextFieldからプロパティに書き込むのではなく、ObservableObjectのメソッドを指定して引数付きで呼び出せるのが良い用に思えるが、そんなん考えなくてもプロパティでいいやんという気もする
- プロパティだとinputが外部から読まれる仕組みにならざるを得ない