はじめに
SwiftUIのTextFieldにViewModelの値を渡す際にCannot convert value of type 'Published<String>.Publisher' to expected argument type 'Binding<String>'
とエラーが出ました。
つまり、View側ではBinding<String>
の値を期待していたところ、Published<String>.Publisher
だったよということで
誤ってviewModel.$text
としていたところを$viewModel.text
にすれば簡単に解決するのですが
気になったのでviewModel.$text
はどのような場合に使う可能性があるのか調べてみました。
環境
Xcode 14.3
内容
まずは、$viewModel.text
とviewModel.$text
を両方使うサンプルコードを書いてみました。
サンプルコードでは、TextFieldで文字を入力するとViewModelのtextが更新され、その値を.onReceive
して別のTextとして表示しています
(実際にはText(viewModel.text)
とすればそのまま値を表示できるのであえてのサンプルです)

struct ContentView: View {
@ObservedObject var viewModel = ContentViewModel()
@State var onReceiveText: String = ""
var body: some View {
VStack {
TextField("", text: $viewModel.text)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1)
)
Text(onReceiveText)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1)
)
}
.padding(10)
.onReceive(viewModel.$text) { text in
onReceiveText = text
}
}
}
class ContentViewModel: ObservableObject {
@Published var text: String = "TextField"
}
TextFieldに渡すViewModelの値はBinding<String>
なので、$viewModel.text
になります。
ViewModelのtextの値を.onReceive
する際に必要な値はPublisher
なのでviewModel.$text
にします
(参考)
func onReceive<P>(
_ publisher: P,
perform action: @escaping (P.Output) -> Void
) -> some View where P : Publisher, P.Failure == Never
つまり
-
$viewModel.text
はviewModelのtextプロパティへのバインディングを表し、値の読み取りと書き込みの両方ができ、値が変更されるとビューが再描画される -
viewModel.$text
はviewModelのtextプロパティの変更を監視し、その変更を購読できるPublisher
が提供される
ということでした
おまけ
.onReceive
で値を受け取る必要があるように、サンプルコードに少し修正を加えます
struct ContentView: View {
@ObservedObject var viewModel = ContentViewModel()
@State var onReceiveText: String = ""
var body: some View {
VStack {
TextField("", text: $viewModel.text)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1)
)
Text(onReceiveText)
.padding(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1)
)
}
.padding(10)
- .onReceive(viewModel.$text) { text in
+ .onReceive(viewModel.textPublisher) { text in
onReceiveText = text
}
}
}
class ContentViewModel: ObservableObject {
@Published var text: String = "TextField"
+ var textPublisher: AnyPublisher<String, Never> {
+ $text
+ .debounce(for: 1, scheduler: RunLoop.main)
+ .removeDuplicates()
+ .eraseToAnyPublisher()
+ }
}
TextFieldの変更を1秒間ごとの変化でText表示する形にしてみました
この形であれば実装する可能性もあるかなと考えましたが、@Published
ではない値には$マーク不要になるためやはりviewModel.$text
を使うケースはなかなかないのかな
おわりに
使い分けは理解できましたが、なかなかviewModel.$text
をView側で使うことはなさそうだと思いました。
今回は書いていませんが、ObservableObject
の中でObservableObject
を使う場合に、子ObservableObject
からPublisherとして値を受け取るケースなどではviewModel.$text
の形で使うかもなと思いましたので、別の機会に調べてみようと思います!
参考