A Naive Attempt at Writing a View-Model Class With Blocks
I have basically decided not to work on Swift this year with any serious focus. I think that there is so much fluidity within the Apple community regarding how to do Swift well, that I am personally going to be best served by waiting a year. [1]
That said, there are a few things that will simultaneously make me a better programmer and probably get me closer to ready for Swift. In particular, really understanding blocks (i.e. closures) and also moving towards functional patterns in my code. With regards to blocks, I certainly “understand” them, but I think building my intuition around the idea of passing blocks of code is still a work in progress. I was additionally inspired by this nice post by [Khanlou][8Patterns] that does a great job quickly walking through a variety of design patterns that help with ViewController bloat.
So, I fiddled around with building a view-model class based on a core concept ripped off directly from Ken Ferry’s Tunable Spec code. Tunable spec is a simple drop-in view to fine-tune properties in your application in real-time. It does this by having a series of objects, each of which has a single value modified by some control (slider, switch, etc.) and programmer can bind any property in their code to that value through a block that is exposed by the “tunable” object (spec).
[spec withBoolForKey:@"ShowBackgroundColors" owner:self maintain:^(id owner, BOOL flag) {
[[owner verticalMovementView] setBackgroundColor:flag ? [UIColor magentaColor] : nil]; //owner in this case is a viewController. Flag is the boolean property of a UISwitch
}];
There is a block for each property and these blocks are managed by an [NSMapTable][NSMapTableRef] in the parent object.
I modeled my object on this behavior, but tried a slightly different tack. The maintain object can be subclassed and any number of properties can be added to the header interface. Changes in these properties can trigger blocks as defined below.
This is a lot like KVO bindings, but with a little more control, and without using KVC coding, which I dislike because of the use of NSString specifiers.[2]
My modifications to the original code were as follows:
- I added the viewModel object itself as a parameter passed (weakly) into the block.
- I renamed “owner” as “dependentObject” to be explicit about the fact that it is the object that should (best practice) change based on changes to the model object. (Of course, this can’t be enforced because anything can be put into a block).
You can set a bound block can be triggered according to 4 specifications:
Any property in the object changes. (A little dangerous because two different interacting viewModels might be written to trigger each others blocks and setters. So I also made a modified method which asserts that the dependent object is a UIView. This way, setter->block call->setter cycles won’t be created.)
[modelObject updateView:self withBlock:^(id dependentObject, id model) { UILabelVM *vm = model; axisLabel *l = (axisLabel *)dependentObject; l.text = vm.vmText; l.backgroundColor = vm.vmBackgroundColor; l.layer.cornerRadius = vm.vmCornerRadius; l.layer.masksToBounds = YES; }];
A specific property changes. (block called automatically when property is set)
[self.radiusSliderVM whenPropertyChanges:propertyKeyPath(currentValue) updateObject:self.demoViewModel withBlock:^(id dependentObject, id model) { [dependentObject setVmRadius:[model currentValue]]; }];
A specific property begins or ends changing. (block called imperatively)
- (void)whenProperty:(NSString *)propertyName startsOrStopsChanging:(valueChange)startOrStop updateBlock:(void (^)(id dependentObject, id model))maintenanceBlock;
I wrote a simple app in which the view controller tells views to bind to the view models:
[self.cornerRadiusMaxLabel configureWithModel:self.crMinLabelVM];
[self.demoView configureWithModel:self.demoViewModel];
[self.radiusSlider configureWithModel:self.radiusSliderVM];
[self.cornerRadiusSlider configureWithModel:self.cornerRadiusSliderVM];
the view model does the binding within this method of the view:
- (void)configureWithModel:(UILabelVM *)modelObject {
modelObject.vmFrame = self.frame;
modelObject.vmTextColor = self.textColor;
modelObject.vmBackgroundColor = self.backgroundColor;
[modelObject updateView:self withBlock:^(id dependentObject, id model) {
UILabelVM *vm = model;
axisLabel *l = (axisLabel *)dependentObject;
l.text = vm.vmText;
l.backgroundColor = vm.vmBackgroundColor;
l.layer.cornerRadius = vm.vmCornerRadius;
l.layer.masksToBounds = YES;
}];
}
the view model initial properties and relationships to each other are set in the viewController
//set demoViewModel properties based on any changes to sliders
[self.radiusSliderVM whenPropertyChanges:propertyKeyPath(currentValue) updateObject:self.demoViewModel withBlock:^(id dependentObject, id model) {
[dependentObject setVmRadius:[model currentValue]];
}];
[self.cornerRadiusSliderVM whenPropertyChanges:propertyKeyPath(currentValue) updateObject:self.demoViewModel withBlock:^(id dependentObject, id model) {
[dependentObject setVmCornerRadius:[model currentValue]];
}];
...
and then the view models properties are set by the view controller actions
- (IBAction)radiusSliderUpdated:(id)sender {
UISlider *s = (UISlider *)sender;
self.radiusSliderVM.currentValue = (s.value) ?: 1;
}
- (IBAction)touchedRadiusSlider:(id)sender {
[self.radiusSliderVM property:propertyKeyPath(currentValue) stopOrStartChanging:changeStart];
}
It seems like a good way of separating concerns though I am sure that I am taking a very naive approach to all of this. I do think that from the point of view of an Objective-C programmer, it is not as foreign as jumping into something like Reactive Cocoa.
There is a lot more polishing that should be done, but it’s probably not a good use of my time at the moment. I think that I have learned what I needed to out of this mini-project. I am sure that I have more to learn conceptually about functional programming, and view models both before further efforts from me are going to make this better.
However, I am definitely going to try using this on my next app experiments.
[1]: and confirmed – by Ash Furrow
[2]: I use a macro from g8productions, which is ok for now, but still not as elegant as I would like.
[NSMapTableRef]: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/NSMapTable_class/index.html’
[8Patterns]: http://khanlou.com/2014/09/8-patterns-to-help-you-destroy-massive-view-controller/