Building custom Bindings from normal classes

Hello, I've been trying to learn about the Binding mechanism. I have an example where I build a Binding from a set/get class+ReferenceWritableKeyPath. Most of the data flow is working but SwiftUI is not invalidating the toggle and doesn't refresh the toggle status. Any idea?

//
//  ContentView.swift
//  swiftui-play
//
//  Created by Daniel Pereira on 23/10/2019.
//  Copyright © 2019 Daniel Pereira. All rights reserved.
//

import SwiftUI
import Combine

extension Binding {
    
    init<A>(keyPath: ReferenceWritableKeyPath<A, Value>, settings: A) {
        self.init(
            get: { settings[keyPath: keyPath] },
            set: { settings[keyPath: keyPath] = $0}
        )
    }
}

final class ClassA {
    var on: Bool = true
}


struct ContentView: View {
        
    @Environment(\.classA) private var classA
    
     var body: some View {
        
         let toggled = Binding(keyPath: \.on, settings: classA)
            
         return Form {
             Toggle(isOn: toggled) {
                 Text("Show welcome text")
                 }
             }.padding(5)
          }
}


let classA = ClassA()

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environment(\.classA, classA)
    }
}


struct ClassAEnvironmentKey: EnvironmentKey {
    static var defaultValue = ClassA()
}

extension EnvironmentValues {
    var classA: ClassA {
        get { self[ClassAEnvironmentKey.self] }
        set { self[ClassAEnvironmentKey.self] = newValue}
    }
}
1 Like

Try to use EnvironmentObject instead (and so requiring ClassA to be ObservableObject).

final class ClassA: ObservableObject {
    @Published var on: Bool = true
}

struct ContentView: View {
    @EnvironmentObject var classA: ClassA
    
    var body: some View {
        let toggled = Binding(keyPath: \.on, settings: classA)
        
        return Form {
            Toggle(isOn: toggled) {
                Text("Show welcome text")
            }
        }.padding(5)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .environmentObject(ClassA())
    }
}

In this particular example, your binding simply becomes $classA.on

struct ContentView: View {
    @EnvironmentObject var classA: ClassA
    
    var body: some View {
        Form {
            Toggle(isOn: $classA.on) {
                Text("Show welcome text")
            }
        }.padding(5)
    }
}

Your custom Binding still works though.

1 Like

I know about EnvironmentObject, just curious about why the other approach doesn't invalidate the view.

1 Like

You need something to signal the view that information has changed. State, ObservedObject, and EnvironmentObject seems to be doing this internally.

EnvironmentValues doesn’t seem to have any similar mechanism. They don’t even need the values to conform to ObservableObject. So my guess is, it’s not for that purpose?

1 Like

Ok, that makes sense! I was thinking that maybe it was the Binding object that was responsible for invalidating the Toggle in this case. But I see that is the State, ObservedObject or EnvironmentObject that does it with some background magic.

1 Like