Is didSet on a #Binding file specific? - swift

Essentially, I'm nesting #Binding 3 layers deep.
struct LayerOne: View {
#State private var doubleValue = 0.0
var body: some View {
LayerTwo(doubleValue: $doubleValue)
}
}
struct LayerTwo: View {
#Binding var doubleValue: Double {
didSet {
print(doubleValue)
}
}
var body: some View {
LayerThree(doubleValue: $doubleValue)
}
}
struct LayerThree: View {
#Binding var doubleValue: Double {
didSet {
print(doubleValue) // Only this print gets run when doubleValue is updated from this struct
}
}
var body: Some view {
// Button here changes doubleValue
}
}
Whichever struct I change doubleValue in is the one where the didSet will get run, so for example if I change it in LayerThree only that one will print, none of the others will.
I am able to watch for changes with .onChange(of: doubleValue) which will then get run when it changes but it's not making sense to me why didSet won't get run except on the struct where it's changed from.
Is #Binding struct specific?

Using property observers like didSet on values wrapped in PropertyWrappers will not have the "normal" effect because the value is being set inside the wrapper.
In SwiftUI, if you want to trigger an action when a value changes, you should use the onChange(of:perform:) modifier.
struct LayerTwo: View {
#Binding var doubleValue: Double
var body: some View {
LayerThree(doubleValue: $doubleValue)
.onChange(of: doubleValue) { newValue
print(newValue)
}
}
}

To see why this happens, we can unveil the syntactic sugar of property wrappers. #Binding var doubleValue: Double translates to:
private var _doubleValue: Binding<Double>
var doubleValue: Double {
get { _doubleValue.wrappedValue }
set { _doubleValue.wrappedValue = newValue }
}
init(doubleValue: Binding<Double>) {
_doubleValue = doubleValue
}
Whatever you do in didSet will be put after the line _doubleValue.wrappedValue = newValue. It should be very obvious why when you update doubleValue in layer 3, The didSet of doubleValue in layer 2 or 1 doesn't get called. They are simply different computed properties!
swiftPunk's solution works by creating a new binding whose setter sets the struct's doubleValue, hence calling didSet:
Binding(get: { return doubleValue },
set: { newValue in doubleValue = newValue }
// ^^^^^^^^^^^^^^^^^^^^^^
// this will call didSet in the current layer

Now all working:
struct ContentView: View {
var body: some View {
LayerOne()
}
}
struct LayerOne: View {
#State private var doubleValue:Double = 0.0 {
didSet {
print("LayerOne:", doubleValue)
}
}
var body: some View {
LayerTwo(doubleValue: Binding(get: { return doubleValue }, set: { newValue in doubleValue = newValue } ))
}
}
struct LayerTwo: View {
#Binding var doubleValue: Double {
didSet {
print("LayerTwo:", doubleValue)
}
}
var body: some View {
LayerThree(doubleValue: Binding(get: { return doubleValue }, set: { newValue in doubleValue = newValue } ))
}
}
struct LayerThree: View {
#Binding var doubleValue: Double {
didSet {
print("LayerThree:", doubleValue)
}
}
var body: some View {
Text(String(describing: doubleValue))
Button("update value") {
doubleValue = Double.random(in: 0.0...100.0)
}
.padding()
}
}
print results:
LayerOne: 64.58963263686678
LayerTwo: 64.58963263686678
LayerThree: 64.58963263686678

Related

Passing a Binding variable to a SwiftUI view from a function causes didSet observer to no longer fire

I want to be able to dynamically return the right variable to be used as a #Binding in a SwiftUI view however when I create a function to return something as Binding I no longer receive the didSet call on that variable. I'm not sure if this is unsupported behavior or I'm doing something wrong.
Here is an example
struct ContentView: View {
#StateObject var dataStore = DataStore()
var body: some View {
VStack {
Toggle("val1", isOn: $dataStore.val1)
Toggle("val2", isOn: dataStore.boundVal2())
}
}
}
class DataStore: ObservableObject {
#AppStorage("val1")
var val1: Bool = false {
didSet {
print("did set val1")
}
}
#AppStorage("val2")
var val2: Bool = false {
didSet {
print("did set val2")
}
}
func boundVal2() -> Binding<Bool> {
return $val2
}
}
When you toggle the first value you get the didSet call, but when you toggle the second value you don't get it.
It turns out that you need to use a Binding object to pass it back, like so:
func boundVal2() -> Binding<Bool> {
Binding(
get: { self.val2 },
set: { self.val2 = $0 }
)
}

Binding #Binding / #State to the #Publisher to decouple VM and View Layer

I want to decouple my ViewModel and View layer to increase testability of my Views.
Therefore, I want to keep my property states inside view and only init them as I needed.
But I cannot initialize my #Binding or #States with #Published properties. Is there a way to couple them inside init function?
I just add example code below to
instead of
import SwiftUI
class ViewModel: ObservableObject {
#Published var str: String = "a"
#Published var int: Int = 1 { didSet { print("ViewModel int = \(int)")} }
init() {
print("ViewModel initialized")
}
}
struct ContentView: View {
#ObservedObject vM = ViewModel()
var body: some View {
Button(action: { vM.int += 1; print(int) }, label: {
Text("Button")
})
}
}
I want to achieve this without using #ObservedObject inside my view.
import SwiftUI
class ViewModel: ObservableObject {
#Published var str: String = "a"
#Published var int: Int = 1 { didSet { print("ViewModel int = \(int)")} }
init() {
print("ViewModel initialized")
}
}
struct ContentView: View {
#Binding var str: String
#Binding var int: Int
var body: some View {
Button(action: { int += 1; print(int) }, label: {
Text("Button")
})
}
}
extension ContentView {
init(viewModel:ObservedObject<ViewModel> = ObservedObject(wrappedValue: ViewModel())) {
// str: Binding<String> and viewModel.str: Published<String>.publisher
// type so that I cannot bind my bindings to viewModel. I must accomplish
// this by using #ObservedObject but this time my view couples with ViewModel
_str = viewModel.wrappedValue.$str
_int = viewModel.wrappedValue.$int
print("ViewCreated")
}
}
// Testing Init
ContentView(str: Binding<String>, int: Binding<Int>)
// ViewModel Init
ContentView(viewModel: ViewModel)
This way I can't bind them each other, I just want to bind my binding or state properties to published properties.
I have realized that by Binding(get:{}, set{}), I can accomplish that. if anyone want to separate their ViewModel and View layer, they can use this approach:
import SwiftUI
class ViewModel: ObservableObject {
#Published var str: String = "a"
#Published var int: Int = 1 { didSet { print("ViewModel int = \(int)")} }
init() {
print("ViewModel initialized")
}
}
struct ContentView: View {
#Binding var str: String
#Binding var int: Int
var body: some View {
Button(action: { int += 1; print(int) }, label: {
Text("Button")
})
}
}
extension ContentView {
init(viewModel:ViewModel = ViewModel()) {
_str = Binding ( get: { viewModel.str }, set: { viewModel.str = $0 } )
_int = Binding ( get: { viewModel.int }, set: { viewModel.int = $0 } )
print("ViewCreated")
}
}

Passing a bound value as an argument to a SwiftUI 2 View

I've got a radial slider which works with this somewhat wordy code:
struct MyView: View {
#ObservedObject var object: Object
#State var parameter: Double = 0 {
didSet {
object.some.nested?.parameter = Int(parameter)
}
}
var body: some View {
RadialSlider(value: $parameter, label: "my parameter")
.onAppear { parameter = Double(object.some.nested?.parameter ?? 0) }
.onChange(of: parameter) { object.some.nested?.parameter = Int($0) }
}
}
As I need to use this slider multiple times, I'd like to omit the onAppear and onChange lines (with the help of a ViewModifier for example). But I don't know how to go about passing my parameter to the RadialSlider so that it maintains the binding. The type of object.some.nested?.parameter can vary between being an Int and a String.
Alternately, Is there a simpler way to bind the value from my object to my radial slider's UI?
You can initialize parameter in init, like
struct MyView: View {
#ObservedObject var object: Object
#State var parameter: Double { // << no initial value !!
didSet {
object.some.nested?.parameter = Int(parameter)
}
}
init(object: Object) {
self.object = object
_parameter = State(initialValue: Double(object.some.nested?.parameter ?? 0))
}
var body: some View {
RadialSlider(value: $parameter, label: "my parameter")
.onChange(of: parameter) { object.some.nested?.parameter = Int($0) }
}
}

setting computed property in a SwiftUI view doesn't compile

Trying to set the computed property s in a SwiftUI view gets compiler error "Cannot assign to property: 'self' is immutable".
How do I have to I call the setter?
struct Test: View{
#State var _s = "test"
#State var _s2 = true
private var s : String
{ get { _s }
set (new)
{ _s = "no test"
_s2 = false
// do something else
}
}
var body: some View
{ Text("\(s)")
.onTapGesture {
self.s = "anyting" // compiler error
}
}
}
Aha... I see. Just use non mutating set
private var s : String
{ get { _s }
nonmutating set (new)
{ _s = "no test"
_s2 = false
// do something else
}
}
That is, why you already have #State property wrapper in your View.
struct Test: View{
#State var s = "test"
var body: some View {
Text("\(s)")
.onTapGesture {
self.s = "anyting" // compiler error
}
}
}
You able to change s directly from your code because s is wrapped with #State.
this is functional equivalent of the above
struct Test: View{
let s = State<String>(initialValue: "alfa")
var body: some View {
VStack {
Text("\(s.wrappedValue)")
.onTapGesture {
self.s.wrappedValue = "beta"
}
}
}
}
Or if Binding is needed
struct Test: View{
let s = State<String>(initialValue: "alfa")
var body: some View {
VStack {
TextField("label", text: s.projectedValue)
}
}
}

Two-way binding in Swift Combine

I have a progress bar and a text field, both are updated depending on each other's input:
class ViewModel: ObservableObject {
#Published var progressBarValue: Double {
didSet {
textFieldValue = String(progressBarValue)
}
}
#Published var textFieldValue: String {
didSet {
progressBarValue = Double(progressBarValue)
}
}
}
Since updating one updates the other, I end up having an infinite recursion in my code.
Is there a way to workaround this with Combine or plain swift code?
Expanding on my comment, here is a minimal example of a slider and a textfield that both control (and be controlled by) a value via two-way bindings:
class ViewModel: ObservableObject {
#Published var progress: Double = 0
}
struct ContentView: View {
#EnvironmentObject var model: ViewModel
var body: some View {
VStack {
TextField("", value: self.$model.progress, formatter: NumberFormatter())
Slider(value: self.$model.progress, in: 0...100)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(ViewModel())
}
}
Note that I also had to inject a ViewModel instance to my environment on AppDelegate in order for this to work (both on preview & actual app)
Maybe additional checks to avoid loops will work?
#Published var progressBarValue: Double {
didSet {
let newText = String(progressBarValue)
if newText != textFieldValue {
textFieldValue = newText
}
}
}
#Published var textFieldValue: String {
didSet {
if let newProgress = Double(textFieldValue),
abs(newProgress - progressBarValue) > Double.ulpOfOne {
progressBarValue = newProgress
}
}
}