Home > Software engineering >  Combine: update values each other
Combine: update values each other

Time:01-03

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)")
        })
    }
}

CodePudding user response:

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)")
        })
    }
}

CodePudding user response:

I guess your problem is that you create an infinite loop since you're observing two values that change each other.

  1. Inches change -> Centimeters gets updated
  2. Centimeters change -> Inches gets updated
  3. Inches change -> Centimeters gets updated
  4. Centimeters change -> Inches gets updated
  5. ... and so on

@jnpdx answer is the right answer imho (it allows you to have a single source of truth)

Alternatively (maybe can be useful for other use cases), you could check whether values actually change so that you could avoid to trigger a useless update.

First make your structs conforming to Equatable

struct Centimeters: Equatable {
    // ...
}

struct Inches: Equatable {
    // ...
}

Then, in your view model apply a modifier to the publisher to avoid triggering the event if value is not actually changed.

$centimeters
    .removeDuplicates()
    .sink {
        self.inches.value = $0.updateInches()
    }.store(in: &cancellables)
$inches
    .removeDuplicates()
    .sink {
        self.centimeters.value = $0.updateCentimeters()
    }.store(in: &cancellables)
  • Related