MVVM With ReactiveCocoa

MVVM With ReactiveCocoa

[[164687]]

MVVM is a software architecture pattern, a variation of Martin Fowler's Presentation Model. It was first proposed by Microsoft architect John Gossman in 2005 and applied in Microsoft's WPF and Silverlight software development. MVVM is derived from MVC and is an evolution of MVC. It promotes the separation of UI code and business logic.

Note: This article will combine theory with practice, focusing on an iOS open source project MVVMReactiveCocoa developed using MVVM and RAC, in the hope of helping you practice MVVM. However, before officially starting the article, please think about the following three questions:

  • What are the similarities and differences between MVC and MVVM, and how did MVC evolve to MVVM?
  • What role does RAC play in MVVM? Should MVVM be used in conjunction with RAC?
  • How to transform an existing MVC application into an MVVM application and what should be paid attention to.

With the above questions, let’s get into the main text.

Glossary: ​​RAC in this article is the abbreviation of ReactiveCocoa.

MVC

MVC is the most common architectural pattern used in iOS development and is also the architectural pattern officially recommended by Apple. MVC stands for Model-view-controller, and the relationship between them is as follows:

Yes, MVC looks great, model represents data, view represents UI, and controller is responsible for coordinating the relationship between them. However, although technically view and controller are independent of each other, in fact they almost always appear in pairs, a view can only be matched with a controller, and vice versa. In this case, why don't we regard them as a whole:

Therefore, M-VC may be a more accurate interpretation of the MVC model in iOS. In a typical MVC application, the controller often becomes bloated due to carrying too much logic, so MVC is often ridiculed as Massive View Controller:

iOS architecture, where MVC stands for Massive View Controller.

Frankly speaking, some logic does belong to the controller, but some logic should not be placed in the controller. For example, converting NSDate in the model into NSString that can be displayed by the view. In MVVM, we call these logics presentation logic.

MVVM

Therefore, a good way to solve the Massive View Controller problem is to extract the display logic in the controller and place it in a special place, which is the viewModel. In fact, as long as we put VM between the M-VC in the above figure, we can get the structure diagram of the MVVM mode:

From the above figure, we can clearly see the relationship between the four components in MVVM. Note: In addition to view, viewModel and model, there is also a very important implicit component binder in MVVM:

  • View: It consists of the view and controller in MVC, responsible for UI display, binding properties in viewModel, and triggering commands in viewModel;
  • ViewModel: The display logic extracted from the MVC controller is responsible for obtaining the data required by the view from the model, converting it into data that the view can display, and exposing public properties and commands for the view to bind;
  • Model: consistent with the model in MVC, including data model, database access operations and network requests, etc.;
  • Binder: In MVVM, declarative data and command binding is an implicit convention that allows developers to easily synchronize view and viewModel, avoiding writing a lot of complicated boilerplate code. In Microsoft's MVVM implementation, a markup language called XAML is used.

ReactiveCocoa

Although, in iOS development, the system does not provide a similar framework that allows us to easily implement the binder function, fortunately, GitHub's open source RAC gives us a very good choice.

RAC is a functional responsive programming framework in iOS. It is inspired by Functional Reactive Programming and is a byproduct of Justin Spahr-Summers and Josh Abernathy's development of GitHub for Mac. It provides a series of APIs for combining and transforming value streams. For more information about RAC, you can read my previous article "ReactiveCocoa v2.5 Source Code Analysis Architecture Overview".

In the MVVM implementation of iOS, we can use RAC to act as a binder between view and viewModel to elegantly synchronize the two. In addition, we can also use RAC in the model layer, using Signal to represent asynchronous data acquisition operations, such as reading files, accessing databases, and network requests. This means that the latter application scenario of RAC is unrelated to MVVM, that is, we can also use it in the model layer of MVC.

summary

To sum up, we only need to extract the display logic from the controller in MVC and place it in the viewModel, and then use certain technical means, such as RAC, to synchronize the view and viewModel to complete the transformation from MVC to MVVM.

Talk is cheap. Show me the code.

Next, let's go directly to the code and look at an example of converting the MVC model to the MVVM model. First, the code of the model layer Person:

  1. @interface Person : NSObject
  2.   
  3. - (instancetype)initwithSalutation:(NSString *)salutation firstName:(NSString *)firstName lastName:(NSString *)lastName birthdate:(NSDate *)birthdate;
  4.   
  5. @property (nonatomic, copy, readonly) NSString *salutation;
  6. @property (nonatomic, copy, readonly) NSString *firstName;
  7. @property (nonatomic, copy, readonly) NSString *lastName;
  8. @property (nonatomic, copy, readonly) NSDate *birthdate;
  9.   
  10. @end  

Then comes the view layer code PersonViewController. In the viewDidLoad method, we convert the attributes in Person and assign them to the corresponding view for display:

  1. - ( void )viewDidLoad {
  2. [ super viewDidLoad];
  3.   
  4. if (self.model.salutation.length > 0 ) {
  5. self.nameLabel.text = [NSString stringWithFormat:@ "%@ %@ %@" , self.model.salutation, self.model.firstName, self.model.lastName];
  6. } else {
  7. self.nameLabel.text = [NSString stringWithFormat:@ "%@ %@" , self.model.firstName, self.model.lastName];
  8. }
  9.   
  10. NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  11. [dateFormatter setDateFormat:@ "EEEE MMMM d, yyyy" ];
  12. self.birthdateLabel.text = [dateFormatter stringFromDate:model.birthdate];
  13. }

Next, we introduce a viewModel and extract the display logic in PersonViewController into this PersonViewModel:

  1. @interface PersonViewModel : NSObject
  2.   
  3. - (instancetype)initWithPerson:(Person *)person;
  4.   
  5. @property (nonatomic, strong, readonly) Person *person;
  6. @property (nonatomic, copy, readonly) NSString *nameText;
  7. @property (nonatomic, copy, readonly) NSString *birthdateText;
  8.   
  9. @end  
  10.   
  11. @implementation PersonViewModel
  12.   
  13. - (instancetype)initWithPerson:(Person *)person {
  14. self = [ super init];
  15. if (self) {
  16. _person = person;
  17.     
  18. if (person.salutation.length > 0 ) {
  19. _nameText = [NSString stringWithFormat:@ "%@ %@ %@" , self.person.salutation, self.person.firstName, self.person.lastName];
  20. } else {
  21. _nameText = [NSString stringWithFormat:@ "%@ %@" , self.person.firstName, self.person.lastName];
  22. }
  23.     
  24. NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
  25. [dateFormatter setDateFormat:@ "EEEE MMMM d, yyyy" ];
  26. _birthdateText = [dateFormatter stringFromDate:person.birthdate];
  27. }
  28. return self;
  29. }
  30.   
  31. @end  

Finally, PersonViewController will become very lightweight:

  1. - ( void )viewDidLoad {
  2. [ super viewDidLoad];
  3.       
  4. self.nameLabel.text = self.viewModel.nameText;
  5. self.birthdateLabel.text = self.viewModel.birthdateText;
  6. }

How is it? In fact, MVVM is not as difficult as you think, and more importantly, it does not destroy the existing structure of MVC. It just moves some codes, that's all. Well, after saying so much, what are the advantages of MVVM compared to MVC? I think it can be summarized into the following three points:

  • Since the display logic is extracted to the viewModel, the code in the view will become very lightweight;
  • Since the code in the viewModel is independent of the UI, it has good testability;
  • For a model that encapsulates a lot of business logic, changing it may be difficult and risky. In this scenario, the viewModel can be used as an adapter for the model to avoid major changes to the model.

Through the previous examples, we have already had some understanding of the first point; as for the third point, it may be more obvious for a complex and large application; below, we still use the previous example to intuitively feel the benefit of the second point:

  1. SpecBegin(Person)
  2. NSString *salutation = @ "Dr." ;
  3. NSString *firstName = @ "first" ;
  4. NSString *lastName = @ "last" ;
  5. NSDate *birthdate = [NSDate dateWithTimeIntervalSince1970: 0 ];
  6.   
  7. it (@ "should use the salutation available. " , ^{
  8. Person *person = [[Person alloc] initWithSalutation:salutation firstName:firstName lastName:lastName birthdate:birthdate];
  9. PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
  10. expect(viewModel.nameText).to.equal(@ "Dr. first last" );
  11. });
  12.   
  13. it (@ "should not use an unavailable salutation. " , ^{
  14. Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
  15. PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
  16. expect(viewModel.nameText).to.equal(@ "first last" );
  17. });
  18.   
  19. it (@ "should use the correct date format. " , ^{
  20. Person *person = [[Person alloc] initWithSalutation:nil firstName:firstName lastName:lastName birthdate:birthdate];
  21. PersonViewModel *viewModel = [[PersonViewModel alloc] initWithPerson:person];
  22. expect(viewModel.birthdateText).to.equal(@ "Thursday January 1, 1970" );
  23. });
  24. SpecEnd

For MVVM, we can regard the view as the visualization of the viewModel, which provides the data and commands required by the view. Therefore, the testability of the viewModel can help us greatly improve the quality of the application.

MVVMReactiveCocoa

Next, we will move on to the second part of this article, focusing on an open source project MVVMReactiveCocoa developed using MVVM and RAC. Note that this article will mainly introduce the architecture and design ideas of this application, hoping to provide you with a real reference case for practicing MVVM. Some architectures are not necessary for MVVM, but we introduced them to use MVVM more smoothly, especially ViewModel-Based Navigation. Therefore, please make corresponding choices and handle them flexibly in the process of practice based on the actual situation of your own application. Finally, we will take the login interface as an example to explore the practical ideas of MVVM.

Note: The following content is based on the v2.1.1 tag of MVVMReactiveCocoa, and some irrelevant codes have been deleted.

Class Diagram

In order to help us understand the overall structure of MVVMReactiveCocoa from a macro perspective, let's first look at its class diagram:

MVVMReactiveCocoa-v2.1.1

From the above figure, we can see that there are two main inheritance systems in MVVMReactiveCocoa:

  • The inheritance hierarchy of viewModel marked in blue, the base class is MRCViewModel;
  • The inheritance hierarchy of the view marked in red has the base class MRCViewController.

In addition to providing the base class MRCViewModel/MRCViewController corresponding to the system base class UIViewController, it also provides the base classes MRCTableViewModel/MRCTableViewController and MRCTabBarViewModel/MRCTabBarController corresponding to the system base classes UITableViewController and UITabBarController. Among them, the base class MRCTableViewModel/MRCTableViewController is the most commonly used.

Note: The reason why MVVMReactiveCocoa is organized in the form of a base class is that, on the one hand, I am the only main developer and this solution is very easy to implement; on the other hand, the base class method makes it as simple as possible to reuse code and improve development efficiency.

Service Bus

After the previous discussion, we already know that the main responsibility of the viewModel in MVVM is to obtain the data required by the view from the model layer and convert the data into a form that the view can display. Therefore, in order to facilitate the viewModel layer to call all services in the model layer and uniformly manage the creation of these services, I use the abstract factory pattern to centrally manage all services in the model layer. The structure diagram is as follows:

From the above figure, we can see that the service bus class MRCViewModelServices/MRCViewModelServicesImpl mainly includes the following three aspects:

  • The application's own service classes are marked in pomelo yellow, including two service classes: MRCAppStoreService/MRCAppStoreServiceImpl and MRCRepositoryService/MRCRepositoryServiceImpl;
  • The API framework provided by the third-party GitHub is marked in sky blue and mainly includes the OCTClient service class;
  • The navigation service of the application is marked with algae green, including the MRCNavigationProtocol protocol and the implementation class MRCViewModelServicesImpl.

The first two provide services to the viewModel layer in the form of signals, representing asynchronous network requests and other data acquisition operations, while we can obtain the required data in the viewModel layer by subscribing to signals. In addition, the service bus also implements the MRCNavigationProtocol protocol, which is as follows:

  1. @protocol MRCNavigationProtocol [NSObject] (Due to recognition issues, square brackets are used instead of angle brackets)
  2.   
  3. - ( void )pushViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated;
  4.   
  5. - ( void )popViewModelAnimated:(BOOL)animated;
  6.   
  7. - ( void )popToRootViewModelAnimated:(BOOL)animated;
  8.   
  9. - ( void )presentViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated completion:(VoidBlock)completion;
  10.   
  11. - ( void )dismissViewModelAnimated:(BOOL)animated completion:(VoidBlock)completion;
  12.   
  13. - ( void )resetRootViewModel:(MRCViewModel *)viewModel;
  14.   
  15. @end  

Does it look familiar? Yes, the MRCNavigationProtocol protocol is actually defined based on the system's navigation operations and is used to implement ViewModel-Based navigation services. Note that the service bus class MRCViewModelServicesImpl does not actually implement the operations declared in the MRCNavigationProtocol protocol, but only implements some empty operations:

  1. - ( void )pushViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated {}
  2.   
  3. - ( void )popViewModelAnimated:(BOOL)animated {}
  4.   
  5. - ( void )popToRootViewModelAnimated:(BOOL)animated {}
  6.   
  7. - ( void )presentViewModel:(MRCViewModel *)viewModel animated:(BOOL)animated completion:(VoidBlock)completion {}
  8.   
  9. - ( void )dismissViewModelAnimated:(BOOL)animated completion:(VoidBlock)completion {}
  10.   
  11. - ( void )resetRootViewModel:(MRCViewModel *)viewModel {}

So, how do we implement ViewModel-Based navigation operations? What is the purpose of using MRCViewModelServicesImpl to implement these no-ops? Why do we do this and what is the purpose? Brother, don't worry, please continue to read the next section.

ViewModel-Based Navigation

Let's first think about a question, that is, why do we need to implement ViewModel-Based navigation operations? Wouldn't it be better to use the system's push/present operations directly in the view layer to complete the navigation? I have summarized the reasons for doing so, mainly in the following three points:

  • In theory, the application of MVVM mode should be driven by viewModel;

  • According to our previous discussion of MVVM, viewModel provides the data and commands required by view. Therefore, we can often use doNext to perform navigation operations directly after the command is successfully executed, and complete the operation in one go;

  • This can make the view more lightweight, and only needs to bind the data and commands provided by the viewModel.

In this case, how do we implement ViewModel-Based navigation operations? We all know that there are two types of navigation operations in iOS, push/pop and present/dismiss. The former is a function unique to UINavigationController, while the latter is a function available to all UIViewControllers. Note that UINavigationController is also a subclass of UIViewController, so it also has the present/dismiss function. Therefore, in essence, no matter what kind of navigation operation we want to implement, it is ultimately inseparable from push/pop and present/dismiss.

Currently, MVVMReactiveCocoa maintains a NavigationController stack MRCNavigationControllerStack in the view layer. Regardless of push/pop or present/dismiss, the NavigationController at the top of the stack is used to perform navigation operations, and it is ensured that a NavigationController is presented.

Next, let's take a look at the changes in the view hierarchy after MVVMReactiveCocoa performs push/pop or present/dismiss operations. First, let's take a look at the view hierarchy diagram of the application when the user enters the home page after successfully logging in:

At this time, the interface displayed by the application is NewsViewController. There is only one element, NavigationController0, in the MRCNavigationControllerStack stack; and NavigationController1 is not in the MRCNavigationControllerStack stack. This is because the view hierarchy is designed to support the sliding switching of TabBarController, which is a special place on the home page. For more information, you can check the GitHub open source library WXTabBarController. Here, we don't need to care too much about this issue, we just need to understand the principle.

Next, when the user clicks a cell in the NewsViewController interface and enters the warehouse details interface through push, the view hierarchy diagram of the application is as follows:

The application pushes the warehouse details interface to its own stack through the NavigationController0 element at the top of the MRCNavigationControllerStack. At this time, the interface displayed by the application is the pushed warehouse details interface RepoDetailViewController. Finally, when the user clicks the switch branch button in the lower left corner of the warehouse details interface and the branch selection interface pops up through the present method, the view hierarchy diagram of the application is as follows:

The application uses the top element NavigationController0 of the MRCNavigationControllerStack to present NavigationController5. At this time, the application displays the root view SelectBranchOrTagViewController of NavigationController5. Note that since pop and dismiss are the inverse operations of push and present, you only need to read the view hierarchy diagram from bottom to top, and will not repeat them here.

Wait, if I remember correctly, the MRCNavigationControllerStack stack is in the view layer, and the service bus class MRCViewModelServicesImpl is in the viewModel layer. As far as I know, the viewModel layer cannot import anything from the view layer, more strictly speaking, it cannot import anything from UIKit, otherwise it will violate the basic principles of MVVM and lose the testability of the viewModel. Under this premise, how do you make the two related?

Yes, this is the purpose of implementing those empty operations in MRCViewModelServicesImpl. The viewModel calls the empty operations in MRCViewModelServicesImpl to indicate that the corresponding navigation operations need to be performed, and MRCNavigationControllerStack captures these empty operations through Hook, and then uses the NavigationController at the top of the stack to perform the actual navigation operations:

  1. - ( void )registerNavigationHooks {
  2. @weakify (self)
  3. [[(NSObject *)self.services
  4. rac_signalForSelector: @selector (pushViewModel:animated:)]
  5. subscribeNext:^(RACTuple *tuple) {
  6. @strongify (self)
  7. UIViewController *viewController = (UIViewController *)[MRCRouter.sharedInstance viewControllerForViewModel:tuple.first];
  8. [self.navigationControllers.lastObject pushViewController:viewController animated:[tuple.second boolValue]];
  9. }];
  10.   
  11. [[(NSObject *)self.services
  12. rac_signalForSelector: @selector (popViewModelAnimated:)]
  13. subscribeNext:^(RACTuple *tuple) {
  14. @strongify (self)
  15. [self.navigationControllers.lastObject popViewControllerAnimated:[tuple.first boolValue]];
  16. }];
  17.   
  18. [[(NSObject *)self.services
  19. rac_signalForSelector: @selector (popToRootViewModelAnimated:)]
  20. subscribeNext:^(RACTuple *tuple) {
  21. @strongify (self)
  22. [self.navigationControllers.lastObject popToRootViewControllerAnimated:[tuple.first boolValue]];
  23. }];
  24.   
  25. [[(NSObject *)self.services
  26. rac_signalForSelector: @selector (presentViewModel:animated:completion:)]
  27. subscribeNext:^(RACTuple *tuple) {
  28. @strongify (self)
  29. UIViewController *viewController = (UIViewController *)[MRCRouter.sharedInstance viewControllerForViewModel:tuple.first];
  30.   
  31. UINavigationController *presentingViewController = self.navigationControllers.lastObject;
  32. if (![viewController isKindOfClass:UINavigationController. class ]) {
  33. viewController = [[MRCNavigationController alloc] initWithRootViewController:viewController];
  34. }
  35. [self pushNavigationController:(UINavigationController *)viewController];
  36. [presentingViewController presentViewController:viewController animated:[tuple.second boolValue] completion:tuple.third];
  37. }];
  38.   
  39. [[(NSObject *)self.services
  40. rac_signalForSelector: @selector (dismissViewModelAnimated:completion:)]
  41. subscribeNext:^(RACTuple *tuple) {
  42. @strongify (self)
  43. [self popNavigationController];
  44. [self.navigationControllers.lastObject dismissViewControllerAnimated:[tuple.first boolValue] completion:tuple.second];
  45. }];
  46.   
  47. [[(NSObject *)self.services
  48. rac_signalForSelector: @selector (resetRootViewModel:)]
  49. subscribeNext:^(RACTuple *tuple) {
  50. @strongify (self)
  51. [self.navigationControllers removeAllObjects];
  52.   
  53. UIViewController *viewController = (UIViewController *)[MRCRouter.sharedInstance viewControllerForViewModel:tuple.first];
  54. if (![viewController isKindOfClass:[UINavigationController class ]]) {
  55. viewController = [[MRCNavigationController alloc] initWithRootViewController:viewController];
  56. ((UINavigationController *)viewController).delegate = self;
  57. [self pushNavigationController:(UINavigationController *)viewController];
  58. }
  59.   
  60. MRCSharedAppDelegate.window.rootViewController = viewController;
  61. }];
  62. }

By using Hook, we finally realized the ViewModel-Based navigation operation, and did not introduce anything from the view layer into the viewModel layer, thus achieving decoupling.

Router

Another point worth mentioning is that when we call the navigation operation in the viewModel, we only pass in the instance of the viewModel as a parameter. So when we perform the actual navigation operation in the MRCNavigationControllerStack, how can we know which interface to jump to? To this end, we configure a mapping from viewModel to view and agree on a unified method to initialize the view: initWithViewModel:

  1. - (MRCViewController *)viewControllerForViewModel:(MRCViewModel *)viewModel {
  2. NSString *viewController = self.viewModelViewMappings[NSStringFromClass(viewModel. class )];
  3.   
  4. NSParameterAssert([NSClassFromString(viewController) isSubclassOfClass:[MRCViewController class ]]);
  5. NSParameterAssert([NSClassFromString(viewController) instancesRespondToSelector: @selector (initWithViewModel:)]);
  6.   
  7. return [[NSClassFromString(viewController) alloc] initWithViewModel:viewModel];
  8. }
  9.   
  10. - (NSDictionary *)viewModelViewMappings {
  11. return @{
  12. @ "MRCLoginViewModel" : @ "MRCLoginViewController" ,
  13. @ "MRCHomepageViewModel" : @ "MRCHomepageViewController" ,
  14. @ "MRCRepoDetailViewModel" : @ "MRCRepoDetailViewController" ,
  15. ...
  16. };
  17. }

Login screen

Finally, let's take a look at some key codes of viewModel and view in the login interface and discuss the specific practice of MVVM. Note that we will avoid specific business logic as much as possible and focus on the practice of MVVM. The following is a screenshot of the login interface:

The main interface elements are:

  • A button avatarButton for displaying the user's avatar;
  • Input boxes usernameTextField and passwordTextField for entering account and password;
  • A direct login button loginButton and a button browserLoginButton that jumps to the browser authorization login.

Analysis: According to our previous discussion on MVVM, viewModel needs to provide the data and commands required by view. Therefore, the content of MRCLoginViewModel.h header file is as follows:

  1. @interface MRCLoginViewModel : MRCViewModel
  2.   
  3. @property (nonatomic, copy, readonly) NSURL *avatarURL;
  4. @property (nonatomic, copy) NSString *username;
  5. @property (nonatomic, copy) NSString *password;
  6.   
  7. @property (nonatomic, strong, readonly) RACSignal *validLoginSignal;
  8. @property (nonatomic, strong, readonly) RACCommand *loginCommand;
  9. @property (nonatomic, strong, readonly) RACCommand *browserLoginCommand;
  10.   
  11. @end  

It is very intuitive. What needs to be explained in particular is that the validLoginSignal property represents whether the login button is available. It will be bound to the enabled property of the login button in the view. Next, let's take a look at some key codes in the implementation file of MRCLoginViewModel.m:

  1. @implementation MRCLoginViewController
  2.   
  3. - ( void )bindViewModel {
  4. [ super bindViewModel];
  5.   
  6. @weakify (self)
  7. [RACObserve(self.viewModel, avatarURL) subscribeNext:^(NSURL *avatarURL) {
  8. @strongify (self)
  9. [self.avatarButton sd_setImageWithURL:avatarURL forState:UIControlStateNormal placeholderImage:[UIImage imageNamed:@ "default-avatar" ]];
  10. }];
  11.   
  12. RAC(self.viewModel, username) = self.usernameTextField.rac_textSignal;
  13. RAC(self.viewModel, password) = self.passwordTextField.rac_textSignal;
  14. RAC(self.loginButton, enabled) = self.viewModel.validLoginSignal;
  15.   
  16. [[self.loginButton
  17. rac_signalForControlEvents:UIControlEventTouchUpInside]
  18. subscribeNext:^(id x) {
  19. @strongify (self)
  20. [self.viewModel.loginCommand execute:nil];
  21. }];
  22.   
  23. [[self.browserLoginButton
  24. rac_signalForControlEvents:UIControlEventTouchUpInside]
  25. subscribeNext:^(id x) {
  26. @strongify (self)
  27. NSString *message = [NSString stringWithFormat:@ ""%@" wants to open "Safari"" , MRC_APP_NAME];
  28.   
  29. UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil
  30. message:message
  31. preferredStyle:UIAlertControllerStyleAlert];
  32.   
  33. [alertController addAction:[UIAlertAction actionWithTitle:@ "Cancel" style:UIAlertActionStyleCancel handler:NULL]];
  34. [alertController addAction:[UIAlertAction actionWithTitle:@ "Open" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
  35. @strongify (self)
  36. [self.viewModel.browserLoginCommand execute:nil];
  37. }]];
  38.   
  39. [self presentViewController:alertController animated:YES completion:NULL];
  40. }];
  41. }
  42.   
  43. @end  
  • When the username entered by the user changes, the model layer method is called to query the user data cached in the local database and return the avatarURL attribute;
  • When the username or password entered by the user changes, determine whether the length of the username and password are both greater than 0. If so, the login button is enabled, otherwise it is disabled;
  • When the loginCommand or browserLoginCommand command is executed successfully, the doNext code block is called and the resetRootViewModel: method in the service bus is used to enter the home page.

Next, let's take a look at some key codes in MRCLoginViewController:

  1. @implementation MRCLoginViewController
  2. 9  
  3. - ( void )bindViewModel {
  4. [ super bindViewModel];
  5. 9  
  6. @weakify (self)
  7. [RACObserve(self.viewModel, avatarURL) subscribeNext:^(NSURL *avatarURL) {
  8. @strongify (self)
  9. [self.avatarButton sd_setImageWithURL:avatarURL forState:UIControlStateNormal placeholderImage:[UIImage imageNamed:@ "default-avatar" ]];
  10. }];
  11. 9  
  12. RAC(self.viewModel, username) = self.usernameTextField.rac_textSignal;
  13. RAC(self.viewModel, password) = self.passwordTextField.rac_textSignal;
  14. RAC(self.loginButton, enabled) = self.viewModel.validLoginSignal;
  15. 9  
  16. [[self.loginButton
  17. rac_signalForControlEvents:UIControlEventTouchUpInside]
  18. subscribeNext:^(id x) {
  19. @strongify (self)
  20. [self.viewModel.loginCommand execute:nil];
  21. }];
  22. 9  
  23. [[self.browserLoginButton
  24. rac_signalForControlEvents:UIControlEventTouchUpInside]
  25. subscribeNext:^(id x) {
  26. @strongify (self)
  27. NSString *message = [NSString stringWithFormat:@ ""%@" wants to open "Safari"" , MRC_APP_NAME];
  28. 9  
  29. UIAlertController *alertController = [UIAlertController alertControllerWithTitle:nil
  30. message:message
  31. 9 preferredStyle:UIAlertControllerStyleAlert];
  32. 9  
  33. [alertController addAction:[UIAlertAction actionWithTitle:@ "Cancel" style:UIAlertActionStyleCancel handler:NULL]];
  34. [alertController addAction:[UIAlertAction actionWithTitle:@ "Open" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
  35. @strongify (self)
  36. [self.viewModel.browserLoginCommand execute:nil];
  37. }]];
  38. [self presentViewController:alertController animated:YES completion:NULL];
  39. }];
  40. }
  41. 9  

@end

  • Observe the changes of the avatarURL property in the viewModel, and then set the image in the avatarButton;
  • Bind the username and password properties in the viewModel to the contents of the usernameTextField and passwordTextField input boxes respectively;
  • Bind the enabled property of loginButton to the validLoginSignal property of viewModel;
  • When the loginButton and browserLoginButton buttons are clicked, the loginCommand and browserLoginCommand commands are executed respectively.

In summary, after we extract the display logic in MRCLoginViewController into MRCLoginViewModel, the code in MRCLoginViewController becomes more concise and clear. The key point of practicing MVVM is that we must be able to clearly analyze the data and commands that the viewModel needs to expose to the view, and these data and commands can represent the current state of the view.

Summarize

First, we introduced the concepts of MVC and MVVM and the evolution from MVC to MVVM from a theoretical perspective; then, we introduced two usage scenarios of RAC in MVVM; finally, from a practical perspective, we focused on an open source project MVVMReactiveCocoa developed using MVVM and RAC. In general, I think MVVM in iOS can be divided into the following three different levels of practice, which correspond to different applicable scenarios:

  • MVVM + KVO, suitable for existing MVC projects, teams that want to convert to MVVM but do not plan to introduce RAC as a binder;
  • MVVM + RAC, suitable for existing MVC projects, teams that want to convert to MVVM and introduce RAC as a binder;
  • MVVM + RAC + ViewModel-Based Navigation is suitable for brand new projects, teams that want to practice MVVM and plan to introduce RAC as a binder, and also want to practice ViewModel-Based Navigation.

In conclusion, I hope this article can dispel your concerns about the MVVM model and take action now.

Reference Links

  • https://www.objc.io/issues/13-architecture/mvvm/
  • https://msdn.microsoft.com/en-us/library/hh848246.aspx
  • https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel
  • https://medium.com/ios-os-x-development/ios-architecture-patterns-ecba4c38de52#.p6n56kyc4
  • http://cocoasamurai.blogspot.ru/2013/03/basic-mvvm-with-reactivecocoa.html
  • http://www.sprynthesis.com/2014/12/06/reactivecocoa-mvvm-introduction/

<<:  iOS: Let's talk about Designated Initializer

>>:  How to write a good bug report

Recommend

Yan Jie 14-day extreme waist and abdomen shaping

Yan Jie's 14-day ultimate waist and abdomen s...

Xiaohongshu promotion method and Xiaohongshu ranking skills!

Today I will talk about how to promote Xiaohongsh...

The most powerful aid for jungle archaeology - LiDAR

Human beings are small but tenaciously multiplyin...

Will Windows 7 really not work with new CPUs in the future?

A piece of news about Windows 10 these days has ma...

Where has the coronavirus gone? Will it disappear? Experts respond

Recently, the question of "where has the new...

How to plan and promote an excellent event?

Whether you are doing user operations, new media ...

4,200 years ago, Chinese ancestors were already driving cars?

The Pingliangtai Site in Huaiyang was selected as...