Let's say you have a custom control similar to NSSlider but with support for choosing a range of values, instead of a single value. The obvious choice of properties is minValue, maxValue, leftValue, rightValue, or however you wanna name them.
You'd probably also want to make sure that leftValue and rightValue always lay in between minValue and maxValue. Same for minValue and maxValue. You don't want them to potentially invalidate the existing leftValue and rightValue, like by—let's say—setting maxValue to a value lower than the current rightValue.
Pretty straight forward so far.
What however do you do in case of ensuring proper KVO accessibility/compliency? You simply can't assure that KVO sets the properties in the proper order (that being first setting the min and max limits and then the other values).
I happen to have such an UI element and simply can't figure out hot to get this running without opening the doors for falsely set values.
The custom control is meant to be bound to a model object. If I now create such a model with the values (min: 0.00 max: 42.00 left: 14.00 right: 28.00) I end up with the following setup in the custom control (min: 0.00 max: 42.00 left: 1.00 right: 1.00)
The reason for this is that instead of calling minValue and maxValue first, it calls leftValue and rightValue first, which causes both values to end up with 1.0 (which is the default maxValue). (After which the maxValue is then set to its proper value.)
//[self prepare] gets called by both, [self initWithCoder:] and [self initWithFrame:]
- (void)prepare
{
minValue = 0.0;
maxValue = 1.0;
leftValue = 0.0;
rightValue = 1.0;
}
- (void)setMinValue:(double)aMinValue
{
if (aMinValue < maxValue && aMinValue < leftValue) {
minValue = aMinValue;
[self propagateValue:[NSNumber numberWithDouble:minValue] forBinding:@"minValue"];
[self setNeedsDisplay:YES];
}
}
- (void)setMaxValue:(double)aMaxValue
{
if (aMaxValue > minValue && aMaxValue > rightValue) {
maxValue = aMaxValue;
[self propagateValue:[NSNumber numberWithDouble:maxValue] forBinding:@"maxValue"];
[self setNeedsDisplay:YES];
}
}
- (void)setLeftValue:(double)aLeftValue
{
double newValue = leftValue;
if (aLeftValue < minValue) {
newValue = minValue;
} else if (aLeftValue > rightValue) {
newValue = rightValue;
} else {
newValue = aLeftValue;
}
if (newValue != leftValue) {
leftValue = newValue;
[self propagateValue:[NSNumber numberWithDouble:leftValue] forBinding:@"leftValue"];
[self setNeedsDisplay:YES];
}
}
- (void)setRightValue:(double)aRightValue
{
double newValue = leftValue;
if (aRightValue > maxValue) {
newValue = maxValue;
} else if (aRightValue < leftValue) {
newValue = leftValue;
} else {
newValue = aRightValue;
}
if (newValue != rightValue) {
rightValue = newValue;
[self propagateValue:[NSNumber numberWithDouble:rightValue] forBinding:@"rightValue"];
[self setNeedsDisplay:YES];
}
It's times like these when you wish Apple had opened up the source of Cocoa. Or at least some of the controls for deeper inspection.
How would you have implemented such a control?
Or more general: What's the best practice for implementing a class that does have cross-dependent properties while complying to KVO?
Wow. This is a tricky problem. As you've already identified, there's nothing you can do to ensure that KVO messages are sent in any particular order.
Here's one thought: in your setLeftValue:
and setRightValue:
accessors, instead of trimming the value, why not just store the value that's passed in, and then trim the value on access.
For example:
- (void)setLeftValue:(double)value {
leftValue = value;
[self propagateValue:[NSNumber numberWithDouble:self.leftValue] forBinding:@"leftValue"]; // Note the use of the accessor here
[self setNeedsDisplay:YES];
}
- (double)leftValue {
return MAX(self.minValue, MIN(self.maxValue, leftValue));
}
This way, once minValue
and maxValue
are set, leftValue
and rightValue
will represent the correct values. And, by using custom accessors, you can ensure that your constraint that minValue <= leftValue/rightValue <= maxValue
is preserved.
Also, for the code you're writing, the MIN
and MAX
preprocessor macros will save you a lot of effort.