MVVM works best with a binding mechanism.As we have seen before, MVVM is basically the optimized version of MVC, so it is easy to see how it is integrated into existing applications using the typical MVC Architecture. Let's look at a simplePerson
Model and corresponding View Controller:
@interface Person : NSObject- (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;@property (nonatomic, readonly) NSString *salutation;@property (nonatomic, readonly) NSString *firstName;@property (nonatomic, readonly) NSString *lastName;@property (nonatomic, readonly) NSDate *birthdate;@end
Cool! Now let's assume that we havePersonViewController
, InviewDidLoad
, You only needmodel
Set some labels for properties.
- (void)viewDidLoad { [super viewDidLoad]; if (self.model.salutation.length > 0) { self.nameLabel.text = [NSString stringWithFormat:@"%@ %@ %@", self.model.salutation, self.model.firstName, self.model.lastName]; } else { self.nameLabel.text = [NSString stringWithFormat:@"%@ %@", self.model.firstName, self.model.lastName]; } NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"]; self.birthdateLabel.text = [dateFormatter stringFromDate:model.birthdate];}
This is all straightforward, standard MVC. Now let's take a look at how we can use a View Model to enhance it.
@interface PersonViewModel : NSObject- (instancetype)initWithPerson:(Person *)person;@property (nonatomic, readonly) Person *person;@property (nonatomic, readonly) NSString *nameText;@property (nonatomic, readonly) NSString *birthdateText;@end
The implementation of our View Model is roughly as follows:
@implementation PersonViewModel- (instancetype)initWithPerson:(Person *)person { self = [super init]; if (!self) return nil; _person = person; if (person.salutation.length > 0) { _nameText = [NSString stringWithFormat:@"%@ %@ %@", self.person.salutation, self.person.firstName, self.person.lastName]; } else { _nameText = [NSString stringWithFormat:@"%@ %@", self.person.firstName, self.person.lastName]; } NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; [dateFormatter setDateFormat:@"EEEE MMMM d, yyyy"]; _birthdateText = [dateFormatter stringFromDate:person.birthdate]; return self;}@end
Cool! We haveviewDidLoad
Put the representation logic in our View Model. At this time, our newviewDidLoad
It will be very lightweight:
- (void)viewDidLoad { [super viewDidLoad]; self.nameLabel.text = self.viewModel.nameText; self.birthdateLabel.text = self.viewModel.birthdateText;}
So, as you have seen, we have not made too many changes to our MVC Architecture. The Code is the same, but the location is moved. It is compatible with MVC and brings a lighter amount of View Controllers.
Testable, huh? What is it? Well, View Controller is notoriously difficult to test because they have done too many things. In MVVM, we try to move as much code as possible into the View Model. It is much easier to test the View Controller, because they do not do a lot of things, and the View Model is also very easy to test. Let's take a look:
SpecBegin(Person) NSString *salutation = @"Dr."; NSString *firstName = @"first"; NSString *lastName = @"last"; NSDate *birthdate = [NSDate dateWithTimeIntervalSince1970:0]; it (@"should use the salutation available. ", ^{ Person *person = [[Person alloc] initWithSalutation:salutation firstName:firstName lastName:lastName birthdate:birthdate]; PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person]; expect(viewModel.nameText).to.equal(@"Dr. first last"); }); it (@"should not use an unavailable salutation. ", ^{ Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate]; PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person]; expect(viewModel.nameText).to.equal(@"first last"); }); it (@"should use the correct date format. ", ^{ Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate]; PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person]; expect(viewModel.birthdateText).to.equal(@"Thursday January 1, 1970"); });SpecEnd
If we do not move this logic into the View Model, we will have to instantiate a complete View Controller and the accompanying View, and then compare the Lable value in our View. This is not only a troublesome indirect layer, but also a very fragile test. Now, we can modify the view hierarchy as needed without worrying about disrupting our unit tests. The benefits of using MVVM for testing are very clear, and even can be seen from this simple example. In the case of more complex representation logic, this benefit is more obvious.
Note that in this simple example, the Model is immutable, so we can only specify the attribute of our View Model during initialization. For a variable Model, we also need to use some binding mechanisms so that the View Model can update its attributes when the Model behind it changes. In addition, once the Model on the View Model changes, the View attributes also need to be updated. The changes to the Model should be cascade down to enter the View through the View Model.
On OS X, we can use Cocoa binding, but on iOS we do not have such a good configuration available. We thought of KVO (Key-Value Observation), and it did a great job. However, a simple binding requires a lot of sample code, not to mention many attributes that need to be bound. As an alternative, I personally like to use ReactiveCocoa, but MVVM does not force us to use ReactiveCocoa. MVVM is a great model. It is independent of itself, but it performs better when there is a good binding framework.
We have covered a lot of content: the MVVM is derived from common MVC, and how they are compatible with the paradigm. We can observe the MVVM from a testable example, we can see that MVVM works better with a paired binding mechanism. If you are interested in learning