Multiple direction data binding in iOS - ChangeChannel
Story begins
Syncing data is one of the most common senaria in iOS development.
- Sync data from data model to view
- Update controls whose values are connected with some logic.
KVO was the solution for one way binding, A ====> B, but not A <=====> B. In my case, even A <======> B <======> C is quite common.
There is a feature in FlexibleProto: Move and resize the view:
User able to change the view’s frame by drag it in a natual way. It is efficient but unprecise, (almost) no one can control the drag in 1pt, so there is a property panel, which user can directly modify the value. The data are synced real time between view and panel, e.g, by moving left 10pt in the view, the property panel’s x will be -=10. Vise versa.
Since the change can be sourced from view and also property panel, so direct KVO can’t help, it will create a KVO loop, changes on A will trigger changes on B and then A B
Moves on
At first, I wrap the change code in one function
// Bad one
-(void) updateFrame:(CGRect)frame{
view.frame = frame;
propertyPanel.value = frame;
}
The code is tightly coupled, in order update the frame, I have to call a function defined else where. If I want to add a third view in the play yard, I have to change this function, it will become a headache when more logic added. After playing 10 minutes PunchQuest, I know what I want:
// Ideal one
Create a change bus
attach view on its frame property
attach propertyPanel on its value property
view's frame change will update propertyPanel's value & vise versa
Getting excited
Here is what I get after some work ( with some naive implementation ):
// What I get
// create the channel with empty items and nil init value
ChangeChannel *changeChannel = [[ChangeChannel alloc] initWithItems:@[] value:nil];
// attach view, property channel
[changeChannel attachChangeItem:[[ObjectChangeItem alloc] initWithObject:view keyPath:@"frame"]];
[changeChannel attachChangeItem:[[ObjectChangeItem alloc] initWithObject:propertyPanel keyPath:@"value"]];
// from now on, the view.frame == propertyPanel
Also create a block changeItem, which logs out each frame change
[changeChannel attachChangeItem:[[BlockChangeItem alloc] initWithBlock:^(id newValue, id oldValue){
NSLog(@"Frame changed from: %@ to %@", newValue, oldValue);
}]];
// From now on, every frame change in both view1, propertyPanel will be logged out
Block can’t fire changes but only accept it.
What if match different type of values
ObjectChangeItem *doubleSized = [[ObjectChangeItem alloc] initWithObject:view2 keyPath:@"frame"];
// C means Channel, O means Object, C2OBlock used to transform value from channel, O2CBlock used
// to transform value put into channel
doubleSized.convertor = [[ValueConvertor alloc] initWithC2OBlock: ^(id value){
CGRect result = [value CGRectValue];
result.size.width *= 2;
result.size.height *= 2;
return [NSValue valueWithCGRect:result];
} o2cBlock: ^(id value){
CGRect result = [value CGRectValue];
result.size.width /= 2;
result.size.height /= 2;
return [NSValue valueWithCGRect:result];
}];
You can do whatever transformations, type convert, value convert etc.
And never ends
The library is still in early stage, join me to improve it.
[changeChannel removeItem:view1ChangeItem];
[changeChannel removeItem:propertyPanelChangeItem];
[changeChannel removeItem:blockChangeItem];
......