Ryan Sam
Ryan Sam

Reputation: 2997

Swift - How to pass Binding values through View hierarchies?

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

Answers (1)

Cristik
Cristik

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

Related Questions