shingo.nakanishi
shingo.nakanishi

Reputation: 2797

How to rewrite `@Binding` to ViewModel

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

Answers (2)

Adrien
Adrien

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 Views.

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

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

Related Questions