Search code examples
swiftswiftuicombine

Combine: update values each other


The example code here is very simple. Sliders update double values but not the other way around. Using Combine how to update two or more sliders on each other?

struct Centimeters {
    var value: Double
    
    func updateInches() -> Double {
        return value / 2.54
    }
}

struct Inches {
    var value: Double
    
    func updateCentimeters() -> Double {
        return value * 2.54
    }
}

class SizeValueModel: ObservableObject {
    @Published var centimeters: Centimeters
    @Published var inches: Inches
    var cancellables = Set<AnyCancellable>()
    
    init() {
        self.centimeters = Centimeters(value: 1.0)
        self.inches = Inches(value: 0.393701)
        
        $centimeters.sink {
            self.inches.value = $0.updateInches()
        }.store(in: &cancellables)
        
//        $inches.sink {
//            self.centimeters.value = $0.updateCentimeters()
//        }.store(in: &cancellables)
    }
}

struct ContentView: View {
    @StateObject var model = SizeValueModel()
    var body: some View {
        Slider(value: $model.centimeters.value, in: 0...100, label: {
            Text("\(model.centimeters.value)")
        })
        Slider(value: $model.inches.value, in: 0...39.3701, label: {
            Text("\(model.inches.value)")
        })
    }
}

Solution

  • As you can see when you attempt to add your currently-commented second sink, you'll end up with a circular dependency between the inches and centimeters. Instead, I'd suggest you store one value and use a custom binding for the other:

    struct Centimeters {
        var value: Double
    }
    
    class SizeValueModel: ObservableObject {
        @Published var centimeters: Centimeters
        
        var inchesBinding : Binding<Double> {
            .init {
                self.centimeters.value / 2.54
            } set: {
                self.centimeters.value = $0 * 2.54
            }
    
        }
        
        init() {
            self.centimeters = Centimeters(value: 1.0)
        }
    }
    
    struct ContentView: View {
        @StateObject var model = SizeValueModel()
        var body: some View {
            Slider(value: $model.centimeters.value, in: 0...100, label: {
                Text("\(model.centimeters.value)")
            })
            Slider(value: model.inchesBinding, in: 0...39.3701, label: {
                Text("\(model.inchesBinding.wrappedValue)")
            })
        }
    }