Writing in the front

There has been a lot of controversy over software architecture patterns (specifically, software coding specifications or software development patterns) in recent years. The debate is over which architecture is best for MVC, MVVM, and MVP, and which is the most awesome, scalable, and maintainable architecture. I have rarely used MVP architecture in actual projects, and my mastery of MVP is only in the Demo stage. This article focuses on the application of the MVVM architecture in real projects and how to write a binding framework between views and ViewModels without RAC.

The MVVM literacy

MVVM (Model — View — Viewmodel) is a software architecture pattern.

MVVM helps to separate the development of graphical user interfaces from the development of business logic or back-end logic (data models) through markup languages or GUI code. The viewmodel of MVVM is a value translator, which means that the viewmodel is responsible for exposing (transforming) data objects from the model for easy management and rendering of objects. In this regard, the viewmodel does more than the view and handles most of the view’s display logic. The view model implements the mediator pattern, organizing access to the back-end logic of the set of use cases supported by the view.

MVVM is a variant of Martin Fowler’s PM (Presentation Model) design pattern. MVVM abstracts the state and behavior of a view in the same way, but PM abstracts a view (creating a viewmodel) in a way that is independent of a particular user interface platform. Both MVVM and PM come from the MVC pattern.

MVVM was developed by Microsoft architects Ken Cooper and Ted Peters by leveraging WPF (Microsoft). NET graphics system) and Silverlight (an Internet application derivative of WPF) to simplify event-driven programming for user interfaces. John Gossman, one of Microsoft’s WPF and Silverlight architects, published MVVM on his blog in 2005.

MVVM is also referred to as Model-View-Binder, especially when not involved. NET platform implementation. Model-view-binder is used by ZK, a Web application framework written in Java, and KnockoutJS, a JavaScript library.

The above content is from Wikipedia. The MVVM wikipedia.

In short, MVVM is an improved version of MVC. We all know that the MVC software architecture pattern is apple’s recommended development pattern.

The M in MVC is simply the data model retrieved from the network, the V is our view interface, and the C is our ViewController.

In this case, the ViewController is responsible for scheduling between the View and the Model. Interactive events from the View are called back to the ViewController in either target-action or delegate mode. At the same time, the ViewController is also responsible for transmitting the data from Model via KVO and Notification to View for display. As the business becomes more and more complex, the view interaction becomes more and more complex, causing the Controller to become more and more bloated and load forward. He does all the dirty work, and in the end, it’s no good. The result of more than a blessing repair is, not on the reconstruction of you, reconstruction can not replace you. 😅

Here’s a classic MVC architecture diagram from Old Stanford.

So to solve this problem, MVVM came into the picture. He puts both the View and Contrller in the View layer (taking some logic out of the Controller), and the Model layer is still the data Model returned by the server. The ViewModel acts as a UI adapter, meaning that each UI element in the View should find its corresponding property in the ViewModel. In addition, any UI-related logic removed from the Controller is placed in the ViewModel, thus relieving the Controller of its burden.

I simply drew the architecture diagram of MVVM.

From the above architecture diagram, we can clearly sort out their respective division of labor.

  • View layer: View display. UIView and UIViewController, the View layer can hold the ViewModel.
  • ViewModel layer: View adapter. Exposure attributes correspond to the View element’s display content or element state. In general, ViewModel attributes are recommended to be readOnly. We’ll explain why in practice. Also, the ViewModel layer can hold the Model.
  • Model layer: Data Model and persistent abstract Model. The data model is well understood as JSON data pulled back from the server. The persistent abstract Model is temporarily placed in the Model layer because it was not described in detail when MVVM was born. As a rule of thumb, we usually encapsulate database and file operations as Models and provide interfaces to the outside world. (Some companies separate data access operations into a single layer, called the DataAdapter layer, so there are many variations of MVVM in the industry, but it is essentially MVVM).
  • Binder: The soul of MVVM. Unfortunately, MVVM does not have a place in the English word MVVM. Its main function is to do two-way data binding between View and ViewModel. If MVVM does not have Binder, it is not very different from MVC.

We found that the flow of data between the three modules was well controlled because of the clear ownership relationship between the View, ViewModel, and Model.

Here is a recommended blog post for youApe question bank iOS client architecture design, the architecture diagram is as follows.

The architecture of ape question bank is neither MVC nor MVVM in nature. It is an architectural mode of the evolution of the two architectures. The blog post briefly introduces the pros and cons of MVC and MVVM.

  • MVC cons: Massive View Controller, aka fat VC.
  • Disadvantages of MVVM: 1. High learning cost. 2. The DEBUG difficulties.

But there are two things I disagree with about MVVM in the blog post.

  • MVVM is not equal to RAC, so there is no difficulty in debugging MVVM.
  • MVVM is not equal to RAC, so the statement in the blog post that “one of the primary disadvantages of MVVM is that it is expensive to learn and expensive to develop” is also not true.

The MVVM architecture itself is not complicated, and we can still implement View and ViewModel binders through KVO and class KVO without RAC.

As to whether the iOS client architecture design of ape question bank is reasonable, I cannot jump to conclusions because I do not know its specific business. But one thing is for sure: MVVM ≠ RAC.

It’s time for the annual QA session.

Q: Do views and viewModels have to be decoupled? A: A View holds A ViewModel. A ViewModel can’t hold A View. Is that clear? 😅 There is a cost to decoupling, whether by Category or middleware, message chains are invisibly longer, and there is a DEBUG cost.

Q: Why can’t viewModels hold views? A: This is easy to understand, brother Dei, mainly for two reasons: 1.ViewModel testability, namely, unit test is easy to conduct. 2. Team members can develop separately (View and ViewModel development can be done by two people simultaneously).

The MVVM combined with RAC

ReativeCocoa is no stranger, this functional responsive framework has nearly 2W stars on Github. RAC is an excellent framework that can exist independently of MVVM. To think of it only as the View and ViewModel Binder roles in MVVM would be overstating the case. RAC will not be analyzed in this article, but you can try it out for yourself if you are interested.

RAC features:

  • Weird grammar, hybrid. (Functional + responsive programming combination)
  • Everything can be plied. (Event signal RACSignal throughout the framework)
  • Throw the discrete function calls into a bunch 💩. (Personally, I feel a lot like Promise)

Conclusion: RAC is a programming mindset change, so its disadvantages are obvious and the learning cost is high!!

Specific use of RAC, you can refer to the official documentation, self-practice, not expanded here.

MVVM combined with non-RAC (IQDataBinding)

In the MVVM literacy section, we learned that Binder plays the role of View and ViewModel data communicator in MVVM.

For those of you who know Android development, one of the great things about Java is that it’s called annotations. When developing an Android App, you can annotate the binding relationship between a View and a ViewModel in XML. The compiler automatically generates Binder classes for XML and ViewModel during compilation.

Annotations are great, but unfortunately we don’t have them in iOS (Objective-C)!! I am not sure whether Swift has annotations. If you know, please tell me.

Next we will implement a framework for bidirectional binding of View and ViewModel step by step.

Plan 1: “Lying cool method”

The so-called “lie cool method” (really can’t think of a word to describe this most basic method 😅) and KVO, is relative to the ViewModel >>> View.

1.ViewModel >>> View: View does not need to care about the change of the ViewModel property, the View only needs to provide the interface to update the View, after the change of the ViewModel property call the API provided by the View to update the View. So the View doesn’t do too much here, it’s all passive, so I call it “repose.”

2.View >>> ViewModel: When the user operates on a View, such as a switch button, synchronize it to the ViewModel. We know that a View can hold a ViewModel, so we can get the ViewModel pointer directly in the View and update the value by exposing the ViewModel method.

High alert: this most basic method is actually MVC!! He didn’t solve the “Massive View Controller” problem by itself. So in order to not rely on the View in the ViewModel, you have to go through the Controller, and you still have a bunch of glue code. So this solution is not MVVM!! Not deliberately to dig a hole for you, just intended to remind you, when reading the article to draw inferious examples from one another, but not to be confused by some dirty and bad articles 😅😅😅.

Plan 1: KVO

1.ViewModel >>> View: After the ViewModel property changes, inform the View to layout the View. This is most familiar and can be done through KVO.

2.View >>> ViewModel: The user operates the View and updates the value through the update method exposed by the ViewModel (avoid triggering KVO listening when setting the property value, otherwise there will be an infinite loop).

Talk is cheap,show me the code! Let’s take the most familiar Cell as an example. ViewModel

//
//  IQMVVMDemoViewModel.h
//
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface IQMVVMDemoViewModel : NSObject

@property (nonatomic, copy, readonly) NSString *userName;
@property (nonatomic, copy, readonly) NSString *userPwd;

+ (IQMVVMDemoViewModel *)demoViewWithName:(NSString *)userName withPwd:(NSString *)userPwd;
- (void)updateViewModelWithName:(NSString *)userName withPwd:(NSString *)userPwd;

@end

NS_ASSUME_NONNULL_END

Copy the code
//
//  IQMVVMDemoViewModel.m
//  

#import "IQMVVMDemoViewModel.h"

@interface IQMVVMDemoViewModel ()

@property (nonatomic, copy, readwrite) NSString *userName;
@property (nonatomic, copy, readwrite) NSString *userPwd;

@end

@implementation IQMVVMDemoViewModel

+ (IQMVVMDemoViewModel *)demoViewWithName:(NSString *)userName withPwd:(NSString *)userPwd {
    IQMVVMDemoViewModel *viewModel = [[IQMVVMDemoViewModel alloc]init];
    viewModel.userName  = userName;
    viewModel.userPwd   = userPwd;
    return viewModel;
}

- (void)updateViewModelWithName:(NSString *)userName withPwd:(NSString *)userPwd {
    _userName   = userName;
    _userPwd    = userPwd;
}

@end
Copy the code

View

//
//  IQMVVMDemoView.h
//  
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@class IQMVVMDemoViewModel;

@interface IQMVVMDemoView : UITableViewCell

- (void)updateViewWithViewModel:(IQMVVMDemoViewModel *)viewModel;

@end

NS_ASSUME_NONNULL_END
Copy the code
//
//  IQMVVMDemoView.m
//  

#import "IQMVVMDemoView.h"
#import "IQMVVMDemoViewModel.h"

@interface IQMVVMDemoView ()<UITextFieldDelegate>

@property (nonatomic, strong) UITextField *userNameField;
@property (nonatomic, strong) UITextField *userPwdField;
@property (nonatomic, strong) IQMVVMDemoViewModel *viewModel;

@end

@implementation IQMVVMDemoView

#pragma mark--Life Cycle--
- (void)dealloc {
    [self.viewModel removeObserver:self forKeyPath:@"userName"];
    [self.viewModel removeObserver:self forKeyPath:@"userPwd"];
}

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) {
        [self setupSubviews];
    }
    return self;
}

#pragma Public & Private Methods--- (void)setupSubviews { [self.contentView addSubview:self.userNameField]; [self.contentView addSubview:self.userPwdField]; } - (void)updateViewWithViewModel:(IQMVVMDemoViewModel *)viewModel {self.viewModel = viewModel; [self.viewModel addObserver:selfforKeyPath:@"userName" options:NSKeyValueObservingOptionNew context:NULL];
    [self.viewModel addObserver:self forKeyPath:@"userPwd" options:NSKeyValueObservingOptionNew context:NULL];
}

#pragma mark--Delegates & KVO--
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    if ([keyPath isEqualToString:@"userName"]) {
        self.userNameField.text = change[NSKeyValueChangeNewKey];
    } else if([keyPath isEqualToString:@"userPwd"]) { self.userPwdField.text = change[NSKeyValueChangeNewKey]; }} - (void)textField didendediting :(UITextField *)textField {/* update ViewModel*/if (textField == self.userNameField) {
        self.userNameField.text = textField.text;
    } else {
        self.userPwdField.text = textField.text;
    }
    [self.viewModel updateViewModelWithName:self.userNameField.text withPwd:self.userPwdField.text];
}

#pragma mark--Getters & Setters--
- (UITextField *)userNameField {
    if(! _userNameField) { _userNameField = [[UITextField alloc]init]; _userNameField.delegate = self; }return _userNameField;
}

- (UITextField *)userPwdField {
    if(! _userPwdField) { _userPwdField = [[UITextField alloc]init]; _userPwdField.delegate = self; }return _userPwdField;
}


@end

Copy the code

At this point, we have a rough idea of how the View communicates with the ViewModel. But KVO is known to have problems and to write a lot of code (register, remove, receive listener handling) every time you listen for a property. Therefore, plan 1 has the following problems:

  • Direct use of KVO, each time to write a large number of registration, removal and other codes, not automatic removal.
  • If the monitor is not removed, Crash may be directly caused, and the use posture is inconvenient.

Scheme 2: Class KVO (IQDataBinding)

It is called class KVO because scheme two is essentially implemented through KVO. IQDataBinding, however, implements automatic removal, and supports functional and chain calls, which are elegant to use.

How does IQDataBinding work

Controller

/* Introduce NSObject+IQDataBinding header */ - (void)configData {self.contentModel = [[contentModel alloc]init]; self.contentModel.title = @"lobster";
    self.contentModel.content = @"123456"; /* Bind between View and ViewModel */ [self.contentViewbindModel:self.contentModel];
    
}

Copy the code

View

/*ViewModel >>> View*/
- (void)setUpSubviews { [self addSubview:self.loginTextField]; [self addSubview:self.pwdTextField]; self.loginTextField.frame = CGRectMake(0, 0, self.bounds.size.width, 30); self.pwdTextField.frame = CGRectMake(0, 40, self.bounds.size.width, 30); */ __weak Typeof (self)weakSelf = self; */ __weak Typeof (self)weakSelf = self; self.bind(@"title",^(id value){
        weakSelf.loginTextField.text = value;
    }).bind(@"content",^(id value){
        weakSelf.pwdTextField.text = value;
    });
    
}
    
Copy the code
/*View >>> ViewModel*/
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
    [textField resignFirstResponder];
    if(textfield.text) {/* Function calls */ self.update(@"content",textField.text).update(@"title"The @"lobster");
    }
    return YES;
}
Copy the code

IQDataBinding:

  • How can a function support transferring different parameter types when a View updates a ViewModel property?
  • How can a View update its ViewModel without triggering KVO and causing an endless loop?
  • How do I automatically remove KVO?

How can a function support transferring different parameter types when a View updates a ViewModel property?

The author draws on the solution of the navigation framework and solves the problem of transmitting different parameter types by means of macro definition and undetermined parameter. You’ll find the _MASBoxValue function in navigation interesting.

How can a View update its ViewModel without triggering KVO and causing an endless loop?

Obviously, the KVO callback is triggered by the setValue:forKey: function, so my solution is to get IVar and set the value of the instance variable directly. The object_setIvar(ID _Nullable obj, Ivar _Nonnull Ivar, ID _Nullable value) function only accepts values of the ID type. After the Stack Overflow query, it is found that it can be solved by the method of function type coercion.

How do I automatically remove KVO?

This problem is relatively simple, in order to monitor the View dealloc call timing, we can use Hook method, but Hook is not recommended. Especially when hooks like Aspects (which are implemented at a high cost through message forwarding) are used, performance can be severely impacted for business scenarios that call more than 1000 times a second. So my solution is to add an associated object to the View. Since we know that member variables are released before the associated objects are released, we can remove the observer automatically in the dealloc method of the associated object.

/* Add an associated object to the view. Store @{bound Key, callback Block} correspondence. 2. Perform KVO listening based on Key in @{bound Key, callback Block}. 3. Listen to the View Dealloc event and automatically remove the KVO listener. */ IQWatchDog *viewAssociatedModel = objc_getAssociatedObject(self, &kViewAssociatedModelKey);if(! viewAssociatedModel) { viewAssociatedModel = [[IQWatchDog alloc]init]; viewAssociatedModel.target = model; objc_setAssociatedObject(self, &kViewAssociatedModelKey, viewAssociatedModel, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }Copy the code
@interface IQWatchDog : NSObject

@property (nonatomic, weak) id target;
@property (nonatomic, strong) NSMutableDictionary *keyPathsAndCallBacks;

@end

@implementation IQWatchDog

- (void)dealloc {
    [self.keyPathsAndCallBacks enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        [self.target removeObserver:self forKeyPath:key];
    }];
}

- (void)observeKeyPath:(NSString *)keyPath callBack:(observerCallBack)callBack {
    NSAssert(keyPath.length, @"KeyPath is illegal."); /* Load the default value */ id value = [self.target valueForKeyPath:keyPath];if(value) { callBack(value); } / * * / add observer [self keyPathsAndCallBackssetObject:callBack forKey:keyPath];
    [self.target addObserver:self forKeyPath:keyPath options:NSKeyValueObservingOptionNew context:NULL];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
    observerCallBack callBack = self.keyPathsAndCallBacks[keyPath];
    if (callBack) {
        callBack(change[NSKeyValueChangeNewKey]);
    }
}

- (void)removeAllObservers {
    [self.keyPathsAndCallBacks enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        [self.target removeObserver:self forKeyPath:key];
    }];
}

- (NSMutableDictionary *)keyPathsAndCallBacks {
    if(! _keyPathsAndCallBacks) { _keyPathsAndCallBacks = [NSMutableDictionary dictionary]; }return _keyPathsAndCallBacks;
}

@end

Copy the code

Remember the object release process

*/ void *objc_destructInstance(id obj) {void * destructinstance (id obj) {if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if(cxx) object_cxxDestruct(obj); /* If there is a member variable, the member variable is released firstif(assoc) _object_remove_assocations(obj); */ obj->clearDeallocating(); /* Remove weak references from SideTable and set pointer to nil*/}return obj;
}
Copy the code

Making address:IQDataBinding, a framework for bidirectional binding of views and ViewModels

KVOController Simple, Modern, Thread-safe key-value observing for iOS and OS X.

Advice for developers

  • For both new and old teams, and for both new and old projects, I strongly recommend that you try the MVVM architecture, again: MVVM ≠ RAC.
  • For teams with a large number of team members and many legacy issues, I recommend that you try the ARCHITECTURE solution of MVVM+KVO+ data access in the Model layer.
  • Admittedly, RAC is an excellent framework, but landing is difficult, especially in China.
  • Either way, implementation is not an easy task. For MVVM, I recommend a step-by-step strategy, where new work can be developed with MVVM and old code refactored step by step. It also introduces some means of statically checking the code and then implementing the MVVM step by step. I also recommend some dry goods using OCLint custom MVVM rules.

The article was first posted on GitHubGithub.com/Lobster-Kin…