Reputation: 2797
I want to make a ViewModel
for only CustomTextField
below.
import SwiftUI
struct ContentView: View {
@State private var text = ""
var body: some View {
VStack {
CustomTextField(text: $text)
Text(text)
}
}
}
struct CustomTextField: View {
@Binding var text: String
var body: some View {
TextField("title", text: $text)
}
}
I tried to rewrite it as follows, but I couldn't figure out how to rewrite it properly.
import SwiftUI
import Combine
struct ContentView: View {
@State private var text = ""
var body: some View {
VStack {
CustomTextField(text: $text)
Text(text)
}
}
}
struct CustomTextField: View {
@StateObject private var viewModel = CustomTextFieldViewModel()
// @Binding var text: String <-- How to replace ViewModel
var body: some View {
TextField("title", text: $text)
}
}
final class CustomTextFieldViewModel: ObservableObject {
private var cancellables: Set<AnyCancellable> = []
@Published var text = ""
}
If I not use @Binding
, I will not be able to notification with the parents view.
This CustomTextField
is modular and I want it to be able to be used by any parent,
but I don't know how to write it to mimic @Binding
using a ViewModel.(In other words, I want to link ContentView's text
to ViewModel's text
somehow.)
Is there something fundamentally wrong with the way I'm doing this?
Or
Is it possible to solve the problem by using another Property Wrappers
that is not @Published
?
Or
Is there a way to initialize the ViewModel that solves this problem?
Upvotes: 1
Views: 159
Reputation: 1917
If I understand correctly you want a @State
in the parent View (ContentView
) and a ViewModel
in the Child View (CustomTextField
). So you have to declare the @Binding
in your ViewModel :
final class CustomTextFieldViewModel: ObservableObject {
@Binding var text: String
init(text: Binding<String>) {
_text = text
}
}
struct CustomTextField: View {
@ObservedObject private var viewModel: CustomTextFieldViewModel
init(text: Binding<String>) {
viewModel = CustomTextFieldViewModel(text: text)
}
var body: some View {
TextField("title", text: $viewModel.text)
}
}
struct ContentView: View {
@State private var text = ""
var body: some View {
VStack {
CustomTextField(text: $text)
Text(text)
}
}
}
But, you shouldn't use this solution. @Binding
and @State
are only made to be used in View
s.
You could use a single ViewModel, instantiated in the parent view, and shared by the two Views (like in this answer - edit: or in the @workingdog's answer ).
Or, if you want to use two different ViewModels (one to manage the ContentView
and one other for the CustomTextField
) you can use Combine :
struct ContentView: View {
@StateObject private var vm = ContentViewViewModel()
var body: some View {
VStack {
CustomTextField(viewModel: vm.customTextFieldVM)
TextField("", text: $vm.text)
Text(vm.text)
}
}
}
struct CustomTextField: View {
@ObservedObject var viewModel: CustomTextFieldViewModel
var body: some View {
TextField("title", text: $viewModel.text)
}
}
final class ContentViewViewModel: ObservableObject {
@Published var text: String {
didSet {
if text != self.customTextFieldVM.text {
self.customTextFieldVM.text = text
}
}
}
var customTextFieldVM: CustomTextFieldViewModel
var cancellables: Set<AnyCancellable> = []
init() {
text = ""
customTextFieldVM = CustomTextFieldViewModel()
customTextFieldVM.$text
.sink {
if $0 != self.text {
self.text = $0
}
}
.store(in: &cancellables)
}
}
final class CustomTextFieldViewModel: ObservableObject {
@Published var text: String
init() {
text = ""
}
}
Upvotes: 2
Reputation: 36119
to use CustomTextFieldViewModel in CustomTextField you have to declare CustomTextFieldViewModel in the parent, otherwise the "text" data in CustomTextField will not update the parent view. Try something like this:
struct ContentView: View {
@StateObject var viewModel = CustomTextFieldViewModel()
var body: some View {
VStack {
CustomTextField(viewModel: viewModel)
Text(viewModel.text)
}
}
}
struct CustomTextField: View {
@ObservedObject var viewModel: CustomTextFieldViewModel
var body: some View {
TextField("title", text: $viewModel.text)
}
}
final class CustomTextFieldViewModel: ObservableObject {
private var cancellables: Set<AnyCancellable> = []
@Published var text = ""
}
If you want to use only the text part, try this:
struct ContentView: View {
@StateObject var viewModel = CustomTextFieldViewModel()
var body: some View {
VStack {
CustomTextField(text: $viewModel.text)
Text(viewModel.text)
}
}
}
struct CustomTextField: View {
@Binding var text: String
var body: some View {
TextField("title", text: $text)
}
}
Upvotes: 2