How can I connect Binding value of View to Published value of ObservableObject?
The goal: all changes of objValue will be reflected on viewValue and vice versa.
Note: Do not propose direct onChange(obj.objValue) { viewValue = $0 } modifier. It will trigger extra drawing cycle of View (first for objValue and second for viewValue).
class MyObject: ObservableObject {
#Published var objValue: Int = 0
}
struct MyView: View {
#Binding var viewValue: Int
#StateObject var obj = MyObject()
var body: some View {
Text("Placeholder")
.onChange(viewValue) {
//Do something
}
}
}
Here is the working solution (not directly with Combine yet) that is utilising the View adapter that let us to avoid extra redraw of the MyView body.
By passing the Binding value to ValueReader View, only its body will be triggered to redraw, then it is just passing the new result outside and we can work with it. Here we assign the updated value of viewValue to objValue.
This technique is avoiding extra redraw cycles, MyView body will be redrawn only ONCE, no matter if objValue or viewValue was changed first.
Because viewValue is not being used directly in the body, only ValueReader will be redrawn directly on viewValue change skipping MyView's body redraw.
class MyObject: ObservableObject {
#Published var objValue: Int = 0
}
struct MyView: View {
#Binding var viewValue: Int
#StateObject var obj = MyObject()
var body: some View {
ZStack {
ValueReader(value: $viewValue) { newValue in
obj.objValue = newValue //Mirroring viewValue to obj.objValue
}
Text("Placeholder")
.onChange(of: obj.objValue, perform: handleValue)
}
}
private func handleValue(_ value: Int) {
viewValue = value //Mirroring obj.objValue to viewValue
//Do any job here. For example just send analytics
}
private struct ValueReader: View {
#Binding var value: Int
let onChange: (_ newValue: Int) -> ()
var body: some View {
Color.clear
.onChange(of: value) { newValue in
onChange(newValue)
}
}
}
}
Related
The Idea
In one of the views of my application I need to mutate some data. To make the code clear, testable and just to test how far I can get without logic in ViewModels, I've moved the mutating logic to the Model layer.
Say this is my Model
struct Model {
var examples: [Example] = []
/* lots of other irrelevant properties and a constructor here */
}
struct Example: Identifiable {
var id = UUID()
var isEnabled: Bool = true
/* other irrelevant properties and a constructor here */
}
And the function that mutates stuff is
// MARK: mutating methods
extension Model {
mutating func disableExamples(with ids: Set<UUID>) {
// do whatever, does not matter now
}
mutating func enableExamples(with ids: Set<UUID>) {
// do whatever, does not matter now
}
}
Now, let's display it in views, shall we?
struct ContentView: View {
#State private var model = Model()
var body: some View {
VStack {
Text("That's the main view.")
// simplified: no navigation/sheets/whatever
ExampleMutatingView(examples: $model.examples)
}
}
}
struct ExampleMutatingView: View {
#Binding var examples: [Example]
var body: some View {
VStack {
Text("Here you mutate the examples.")
List($examples) {
// TODO: do stuff
}
}
}
}
The attempt
Since I don't want to put the whole Model into the ExampleMutatingView, both because I don't need the whole thing and due to performance reasons, I tried to supply the view with necessary methods.
I've also added the ability to select examples by providing a State variable.
struct ContentView: View {
#State private var model = Model()
var body: some View {
VStack {
Text("That's the main view.")
// simplified: no navigation/sheets/whatever
ExampleMutatingView(examples: $model.examples,
operationsOnExamples: (enable: model.enableExamples, disable: model.disableExamples))
}
}
}
struct ExampleMutatingView: View {
#Binding var examples: [Example]
let operationsOnExamples: (enable: ((Set<UUID>) -> Void, disable: (Set<UUID>) -> Void)
#State private var multiSelection = Set<UUID>()
var body: some View {
VStack {
Text("Here you mutate the examples.")
List($examples, selection: $multiSelection) { example in
Text("\(example.id)")
}
HStack {
Button { operationsOnExamples.enable(with: multiSelection) } label: { Text("Enable selected") }
Button { operationsOnExamples.disable(with: multiSelection) } label: { Text("Disable selected") }
}
}
}
}
The problem
The thing is, with such setup the ContentView greets me with Cannot reference 'mutating' method as function value error. Not good, but mysterious for me for the very reason that fixes it: supplying the actual Model into the view.
The (non ideal) solution
Showing only the parts that changed
// ContentView
1. ExampleMutatingView(model: $model)
// ExampleMutatingView
1. #Binding var model: Model
2. List($model.examples/*...*/)
3. Button { model.enableExamples(with: multiSelection) } /*...*/
4. Button { model.disableExamples(with: multiSelection) } /*...*/
The discussion
Why is it the case? The only difference I see and cannot explain accurately between these two is that supplying the model might give the method access to its self, which is, otherwise, not available. If that's the case, maybe wrapping the methods in some kind of closure with an [unowned self] would help?
I'm fresh to the topic of self in Swift, so I honestly have no idea.
TL;DR: why does it work when I supply the object defining the methods, but does not when I supply only the methods?
I tried to encapsulate the gesture in a class and then call it in another view.
My project can run well, and it can be built well. but Xcode12 give me a pink error. This is my code
class GestureClass: ObservableObject {
private var minZoom:CGFloat=1
private var maxZoom:CGFloat=4.00
#GestureState private var magnificationLevel:CGFloat=1
#Published public var zoomLevel:CGFloat=1
#Published public var current:CGFloat=1
var magnify: some Gesture {
MagnificationGesture()
.updating($magnificationLevel, body:{(value,state,_) in
return state=value
})
.onChanged({ value in
self.current = value
})
.onEnded({ value in
self.zoomLevel = self.setZoom(value)
self.current = 1
})
}
func setZoom(_ gesturevalue:CGFloat) -> CGFloat {
return max(min(self.zoomLevel*gesturevalue, self.maxZoom), self.minZoom)
}
func gestureresult() -> CGFloat {
return self.setZoom(self.current)
}
}
struct GestureModelView: View {
#EnvironmentObject var gesturemodel:GestureClass
var body: some View {
let myges = gesturemodel.magnify
HStack {
Rectangle()
.foregroundColor(.blue)
.frame(width:200*gesturemodel.gestureresult(), height:200)
.gesture(myges)
}
}
}
I deleted this code in the computed property, and the error disappeared. But i really don't know why。
.updating($magnificationLevel, body:{(value,state,_) in
return state=value
})
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.
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
}
}
I want to make a picker use SwiftUI, when I change the value in ChildView, it will not change and called ChildView init.
class ViewModel: ObservableObject {
#Published var value: Int
init(v: Int) {
self.value = v
}
}
struct ChildView: View {
#Binding var value: Int
#ObservedObject var vm = ViewModel(v: 0)
init(value: Binding<Int>) {
self._value = value
print("ChildView init")
}
var body: some View {
VStack {
Text("param value: \(value)")
Text("#ObservedObject bar: \(vm.value)")
Button("(child) bar.value++") {
self.vm.value += 1
}
}
.onReceive(vm.$value) { value = $0 }
}
}
struct ContentView: View {
#State var value = 0
var body: some View {
VStack {
Text("(parent) \(self.value)")
ChildView(value: $value)
}
}
}
But when I remove Text("(parent) \(self.value)") in ContentView, it seems to be normal.
This happens because anytime ChildView gets init-ialized - which happens when ContentView's body is recomputed - it create a new instance of ViewModel with the value 0.
Determine first who "owns" the data. If it's some external object, like ViewModel, then it should get instantiated somewhere where an instance could be longer-lived, for example in ContentView (but it would depend on your real use case):
struct ContentView: View {
#State var value = 0
var childVm = ViewModel(v: 0)
var body: some View {
VStack {
Text("(parent) \(self.value)")
ChildView(vm: childVm, value: $value)
}
}
}
struct ChildView: View {
#Binding var value: Int
#ObservedObject var vm: ViewModel
init(vm: ViewModel, value: Binding<Int>) {
self._value = value
self.vm = vm
print("ChildView init")
}
// ...
}
In general, the described behavior is expected, because source of truth for value is in parent, and updating it via binding you update all places where it is used. That result in rebuild parent body, so recreate child view.
SwiftUI 2.0
Solution is simple - use state object
struct ChildView: View {
#Binding var value: Int
#StateObject var vm = ViewModel(v: 0) // << here !!
// ... other code
SwiftUI 1.0+
Initialize view model with updated bound value
struct ChildView: View {
#Binding var value: Int
#ObservedObject var vm: ViewModel // << declare !!
init(value: Binding<Int>) {
self._value = value
self.vm = ViewModel(v: value.wrappedValue) // << initialize !!
// .. other code
Normally we can use didSet in swift to monitor the updates of a variable. But it didn't work for a #Binding variable. For example, I have the following code:
#Binding var text {
didSet {
......
}
}
But the didSet is never been called.Any idea? Thanks.
Instead of didSet you can always use onReceive (iOS 13+) or onChange (iOS 14+):
import Combine
import SwiftUI
struct ContentView: View {
#State private var counter = 1
var body: some View {
ChildView(counter: $counter)
Button("Increment") {
counter += 1
}
}
}
struct ChildView: View {
#Binding var counter: Int
var body: some View {
Text(String(counter))
.onReceive(Just(counter)) { value in
print("onReceive: \(value)")
}
.onChange(of: counter) { value in
print("onChange: \(value)")
}
}
}
You shouldn’t need a didSet observer on a #Binding.
If you want a didSet because you want to compute something else for display when text changes, just compute it. For example, if you want to display the count of characters in text:
struct ContentView: View {
#Binding var text: String
var count: Int { text.count }
var body: some View {
VStack {
Text(text)
Text(“count: \(count)”)
}
}
}
If you want to observe text because you want to make some other change to your data model, then observing the change from your View is wrong. You should be observing the change from elsewhere in your model, or in a controller object, not from your View. Remember that your View is a value type, not a reference type. SwiftUI creates it when needed, and might store multiple copies of it, or no copies at all.
The best way is to wrap the property in an ObservableObject:
final class TextStore: ObservableObject {
#Published var text: String = "" {
didSet { ... }
}
}
And then use that ObservableObject's property as a binding variable in your view:
struct ContentView: View {
#ObservedObject var store = TextStore()
var body: some View {
TextField("", text: $store.text)
}
}
didSet will now be called whenever text changes.
Alternatively, you could create a sort of makeshift Binding value:
TextField("", text: Binding<String>(
get: {
return self.text
},
set: { newValue in
self.text = newValue
...
}
))
Just note that with this second strategy, the get function will be called every time the view is updated. I wouldn't recommend using this approach, but nevertheless it's good to be aware of it.