[IOS] MVVM-framework introduction, iosmvvm-framework Introduction
I found my first iOS development job in 2011 PX. Although I have been developing iOS outsourcing for several years in college, this is my real iOS development job. I was recruited as the only iOS developer to implement an iPad app with beautiful designs. In just seven weeks, we released 1.0 and continued iteration, adding more features, but in essence, the code library has become more complex.
Sometimes I feel like I don't know what to do. Although I know my design patterns-just like any good programmer-I am so close to what I'm working on that I cannot objectively measure the effectiveness of my architecture decisions. When another developer came in the team, I realized that we were in trouble.
Have you ever heard of MVC? Some people call it Massive View Controller (heavyweight View Controller), which is what we felt at that time. I don't want to introduce the shameful details, but to be honest, if I have to do it again, I will definitely make different decisions.
I will modify a key architecture and bring it into the various applications I have been developing since then, that is, replacing Model-View-Controller with a schema called Model-View-ViewModel.
So what is MVVM? Instead of focusing on explaining the origins of MVVM, let's look at how a typical iOS is built and learn about MVVM from there:
We can see a typical MVC setting. The Model presents data and the View displays the user interface, while the View Controller adjusts the interaction between the two. Cool!
Although View and View Controller are technically different components, they are almost always hand in hand and paired. When do you see that a View can be paired with different View controllers? Or vice versa? So why not normalize their connections?
This more accurately describes the MVC code you may have written. However, it does not do much to solve the problem of increasing heavyweight view controllers in iOS applications. In typical MVC applications, many logics are placed in View Controller. Some of them do belong to the View Controller, but more are the so-called "presentation logic". In MVVM terminology, it is something that converts Model data to View data that can be presented, for example, converting an NSDate into a formatted NSString.
Some things are missing in our illustration, which allow us to put all the representation logics in. We plan to call it "View Model" -- It is located between View/Controller and Model:
It looks much better! This graphic accurately describes what MVVM is: an enhanced version of MVC. We formally connect the view and Controller and move the representation logic from the Controller to a new object, that is, View Model. MVVM sounds complicated, but it is essentially a well-optimized MVC Architecture, which you are already familiar.
Now we know what MVVM is, but why do we want to use it? The motivation for using MVVM on iOS is that it can reduce the complexity of View Controller and make the presentation logic easier to test. Through some examples, we will see how it achieves these goals.
I hope you can take the following three points after reading this article:
MVVM can be compatible with the MVC Architecture you are currently using.
MVVM increases the testability of your application.
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 take a look at a simple Person Model and the corresponding View Controller:
1 2 3 4 5 6 7 8 9 10
|
@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 we have a PersonViewController. In viewDidLoad, you only need to set some labels based on its model attribute.
1 2 3 4 5 6 7 8 9 10 11 12 13
|
- (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.
1 2 3 4 5 6 7 8 9 10
|
@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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
@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 have put the representation logic in viewDidLoad into our View Model. In this case, our new viewDidLoad will be very lightweight:
1 2 3 4 5 6
|
- (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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
|
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