Reputation: 2997
I'm a week in to learning Swift and I'm using SwiftUI to create Views and MVVM to pass data to those views. However because I'm use to React Native in JavaScript I'm a little confused on how to pass data & binding values ("state") in the SwiftUI World
In React Native we have
() -> {
const state = { /* some state */ }
// state logic happens in this parent
(state) -> {
(state) -> ...
(state) -> ...
(state) -> ...
}
(state) -> {
(state) -> ...
(state) -> ...
(state) -> ...
}
}
And so on. So each child has access to the parent state as we pass it down
The idea in React is we have the parent component that holds and manages the state. You can construct complex views/components by putting simpler components together making a hierarchy. But it's the parent or the start of that hierarchy thats the container for that complex view/component which handles the logic of the state.
I tried to follow this same pattern in SwiftUI but instantly ran into problems.
If we had three View
s in SwiftUI:
// Bottom of hierarchy
struct NumberView: View {
var body: some View {
VStack {
Text("\($number)")
Button("ADD", action: { $number += 1 })
}
}
var number: Binding<Int>
}
// Middle of hierarchy
struct TextViewWithNumber: View {
var body: some View {
VStack {
Text(someText)
NumberView(number: $number)
}
}
var someText: String
var number: Binding<Int>
}
// Top of hierarchy
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
TextViewWithNumber(someText: viewModel.string1, number: $viewModel.number1)
TextViewWithNumber(someText: viewModel.string2, number: $viewModel.number2)
}
}
}
struct Model {
var string1: String
var string2: String
var number1: Int
var number2: Int
init() {
string1 = "It's not a motorcycle, baby It's a chopper"
string2 = "Zed's dead"
number1 = 5
number2 = 10
}
}
class ViewModel: ObservableObject {
@Published private var viewModel = Model()
// MARK: - Access to the model
var viewModel: Model {
viewModel
}
// MARK: - Intent(s)
func updateModel() {
// Model Update Code
}
I got a bunch of errors like viewModel is get only
and value type Binding<T> is not of type Binding<T>.Wrapper
and is was basically going in circles at this point.
So, how should you pass ObservableObjects
& Bindings
in a View
hierarchy in SwiftUI?
Similar to passing "state" and "props" in React (if you're familiar with react).
I'm looking for the right way to do this so if whole comparing it to React idea is wrong ignore my comparison.
Upvotes: 1
Views: 2146
Reputation: 32779
SwiftUI and ReactNative are similar, up to a certain point. ReactNative is using the Redux pattern, while SwiftUI, ergh.., allows some nasty shortcuts that iOS developers are kinda "loving" them (@EnvironmentObject
being one of them).
But enough blabbing around, you do have some mistakes in your code that prevent you from using your views as you'd want.
Firstly, there are some incorrect usages of bindings, you should be @Binding var someValue: SomeType
instead of var someValue: Binding<SomeType
, as the compiler provides the $
syntax only for property wrappers (@Binding
is a property wrapper).
Secondly, once you have bindified everything you need (the number
property in your case, you no longer need to reference the properties via $
when reading/writing to them, unless you want to forward them as bindings. Thus, write Text("\(number)")
, and Button("ADD", action: { number += 1 })
.
Thirdly, the @Published
variable needs to be public, and be directly referenced. I assume you attempted some encapsulation there with the computed property, however this will simply not work with SwiftUI - bindings require a two-way street so that the changes can easily propagate. If you want to keep your view model in sync with the changes of the model
property, then you can add a didSet
on that property and do whatever stuff you need to do when the model data changes.
With the above in mind, your code could look something like this:
// Bottom of hierarchy
struct NumberView: View {
var body: some View {
VStack {
Text("\(number)")
Button("ADD", action: { number += 1 })
}
}
@Binding var number: Int
}
// Middle of hierarchy
struct TextViewWithNumber: View {
var body: some View {
VStack {
Text(someText)
NumberView(number: $number)
}
}
var someText: String
@Binding var number: Int
}
// Top of hierarchy
struct ContentView: View {
@ObservedObject var viewModel = ViewModel()
var body: some View {
VStack {
TextViewWithNumber(someText: viewModel.model.string1, number: $viewModel.model.number1)
TextViewWithNumber(someText: viewModel.model.string2, number: $viewModel.model.number2)
}
}
}
struct Model {
var string1: String
var string2: String
var number1: Int
var number2: Int
init() {
string1 = "It's not a motorcycle, baby It's a chopper"
string2 = "Zed's dead"
number1 = 5
number2 = 10
}
}
class ViewModel: ObservableObject {
@Published var model = Model() {
didSet {
// react to descendent views changing the model
updateViewModel()
}
}
// MARK: - Intent(s)
private func updateViewModel() {
// update other properties based on the new state of the `model` one
}
}
Upvotes: 1