Practice of iOS chained syntax data binding lightweight framework

1. Present situation

At present, MVVM design pattern is often used in component-based development. It promotes the separation of UI code and business logic, and solves the bloated problem of viewController to a certain extent, but also makes data binding complex. In many cases, we need to manually bind data and refresh the interface, and often write a pile of scattered data binding business code.

As for the complexity of data binding, we can use ReactiveCocoa framework (a typical functional responsive programming framework). We don't have an in-depth understanding here. Although it is very good and powerful, the supply still exceeds the demand for component-based development. At present, we only need a lightweight data binding framework.

2. Target

Maintain a lightweight data binding open source framework, such as CRDataBind(Chain Response Data Bind). Its interface call supports chain syntax and quickly realizes data binding update through responsive programming.

(this scheme mainly uses the slot machine lottery demo to practice and analyze it. The data binding framework originally comes from: github.com/shidavid/DV... , thank you very much for your contribution

2, Solutions and highlights

1. Programme overview

  • Chain programming is used to support multiple bindings and one-way / two-way data flow;
  • Support filtering, and do not update bound data under certain conditions;
  • Support automatic conversion between numeric value and string, and user-defined data receiving format;
  • As long as the objects that support KVC can implement data binding, it is not limited to View and ViewModel;
  • There is no need to rely on a third party and unbind manually. When the memory of the target object is released, CRDataBind will unbind and release automatically.

2. Problem difficulty

1) How to bind multiple objects at a time through chain syntax?

2) How to realize data binding through responsive programming?

3) How to realize automatic unbinding?

3. Analysis process

1) Chain grammar

In Objective-C, we generally use "[]" to call methods, and simple calls seem acceptable. However, if many layers of calls are superimposed, it is not easy to read. There are often some "]" or "[" error reports missing.

The core of chain syntax is point syntax. In order to enable OC to show the code gracefully and clearly when calling multi-layer methods, we can learn from the point syntax forms of Swift, Masonary, etc.

Example:

    // Swift: get file path
    let path: String = Bundle.main.path(forResource: "image",     ofType: "jpg")!  
​
    // Objective-C: Masonary layout update
    [self.growingButton mas_updateConstraints:^(MASConstraintMaker *make) {
        make.center.equalTo(self);
        make.width.equalTo(@(self.buttonSize.width)).priorityLow();
        make.height.equalTo(@(self.buttonSize.height)).priorityLow();
        make.width.lessThanOrEqualTo(self);
        make.height.lessThanOrEqualTo(self);
    }];
Copy code

The key of point syntax is block, which can learn from the use of Swift closure. Its particularity is that it can help the method pass parameters and return data, so that we can let the method continuously return to the instance itself and continue to call the instance method.

Example:

    /**Binding 3
     model.winCode <---> winCodeLb.text <---> winCodeTF.text
     Add filter to filter the response with the winning number greater than 3 digits
     */
    CRDataBind
    ._inout(self.lotteryVM.currentLottery, @"winCode")
    ._out(self.winCodeLb, @"text")
    ._inout_ui(self.winCodeTF, @"text", UIControlEventEditingChanged)
    ._filter(^BOOL(NSString *text) {
        // Add filter: the winning number does not exceed 3 digits. You cannot configure more than 3 digits on the interface
        return text.length <= 3;
    });
Copy code

2) Responsive programming for data binding

Responsive programming is a programming paradigm oriented to data flow and change propagation. Data input and output (in & out) is the key, binding response refresh. Data inout takes the form of ordinary objects such as target.property = value; UI objects such as textField.text response EditingChanged, etc.

Imagine that in the same chain (response chain), we need an observer. The observer caches the observed objects through weak references. Then, we can use KVO to listen to ordinary objects. When listening to UI objects, we bind the corresponding UI events. Then, when the attribute of an object observed in the chain changes, we can traverse all the observed objects through KVC(setValue:forkey:) Perform the update operation.

3) . realize automatic unbinding

After the above analysis, we can basically realize the interface call and actual data binding. Next, think about it: since there is a binding process, the corresponding unbinding should also be provided, and it is better to unbind automatically, without external manual calls to unbind and release the cache.

How do we trigger the unbundling process? For example, target is the object of data binding. Then the normal logic is that target is released or active calls are used to unbind operation. We need to catch the object release. The existing way is to use the dealloc method, but our purpose is to automatically unbind, so we should not call the binding in all the external objects dealloc bound. You can consider implementing a NSObject classification for all target, and associate a targetModel through runtime. When target is released, model will also release. At this point, we can invoke unbindWithTarget: in targetModel dealloc to unbind and release cache operations.

3, Detailed design

1. Class diagram

2. Code principle analysis

1) A and B are bidirectional data bound, and Ain data changes to update Aout and Bout data. The same is true for Bin.

2) Sometimes, A and B are bound Bi directionally, and B and C are bound Bi directionally. In fact, it is equivalent to A, B and C are bound together on A data Chain chain. Whenever an in data changes, new data is sent to C

On the Chain, the Chain updates all out data.

This enables one-way / two-way data flow.

3) . using KVO and UI(addTarget:) events, the data link is equivalent to observe. Each Observer is marked with a ChainCode. The Observer observes the change of each in data and updates all out data.

4) I. description of main external interfaces

The API called by chain syntax must start with _inout or _in (there must be a data in source, otherwise the follow-up is meaningless). The subsequent binding order can be arbitrary without affecting the binding result.

  • _inout send + receive data
  • _in send data only
  • _out only receives data
  • _cv is returned after custom data conversion
  • _filter conditional filtering
  • _out_key_any bind custom event
  • _out_not the received data is reversed and returned

The specific interfaces are as follows:

#pragma mark - bidirectional binding
+ (DataBindBlock)_inout;
+ (DataBindUIBlock)_inout_ui;
+ (DataBindConvertBlock)_inout_cv;
+ (DataBindUIConvertBlock)_inout_ui_cv;
​
- (DataBindBlock)_inout;
- (DataBindUIBlock)_inout_ui;
- (DataBindConvertBlock)_inout_cv;
- (DataBindUIConvertBlock)_inout_ui_cv;
​
#pragma mark - unidirectional binding - send (data update, only send new data, not receive)
+ (DataBindBlock)_in;
+ (DataBindUIBlock)_in_ui;
​
- (DataBindBlock)_in;
- (DataBindUIBlock)_in_ui;
​
#pragma mark - unidirectional binding - receive (data update, only receive new data, not send)
- (DataBindBlock)_out;
- (DataBindConvertBlock)_out_cv;
- (DataBindBlock)_out_not;
- (DataBindKeyAnyOutBlock)_out_key_any;
​
#pragma mark - filter
- (DataBindFilterBlock)_fil
 Copy code

4, Use example

Setting data binding is generally carried out in the view controller, which can be flexibly used in combination with its own design mode.

Import header file:

#import "CRDataBind.h"
Copy code

Perform one-way / two-way data binding (label - only receive data, model - send and receive data response):

    /**binding
     model.winRate <---> rateLb.text <---> rateSlider.value
     */
    CRDataBind
    ._inout(self.lotteryVM.currentLottery, @"winRate")
    ._inout_ui(self.rateSlider, @"value", UIControlEventValueChanged)
    ._out(self.rateLb, @"text");
Copy code

Filter according to the conditions. If the conditions are not met, the response will not be processed:

    /**binding
     model.winCode <---> winCodeLb.text <---> winCodeTF.text
     Add filter to filter the response with the winning number greater than 3 digits
     */
    CRDataBind
    ._inout(self.lotteryVM.currentLottery, @"winCode")
    ._out(self.winCodeLb, @"text")
    ._inout_ui(self.winCodeTF, @"text", UIControlEventEditingChanged)
    ._filter(^BOOL(NSString *text) {
        // Add filter: the winning number does not exceed 3 digits. You cannot configure more than 3 digits on the interface
        return text.length <= 3;
    });
Copy code

Custom data receiving format:

    /**binding
     model.sn <---> snLb.text <---> self.view.backgroundColor
     The backgroundColor needs to convert the output format
     */
    CRDataBind
    ._inout(self.lotteryVM.currentLottery, @"sn")
    ._out(self.snLb, @"text")
    ._out_cv(self.view, @"backgroundColor", ^UIColor *(NSNumber *num) {
        NSInteger index = num.integerValue % kBGColors.count;
        return kBGColors[index];
    });
Copy code

Bind custom events:

   /**binding
     model.isWin <---> isWinLb.text <---> self.isWin
     Add external custom events to make the lottery number flash after winning
     */
    __weak __typeof(&*self) weakSelf = self;
    CRDataBind
    ._inout(self.lotteryVM.currentLottery, @"isWin")
    ._out(self.isWinLb, @"text")
    ._out_key_any(@"202122", ^(NSNumber *num) {
        weakSelf.isWin = num.boolValue;
        NSLog(@">>>stay setIsWin:The number flashes when the lottery is triggered, iswin = %d", weakSelf.isWin);
    });
Copy code

5, Proof of effectiveness

CRDataBindDemo is made for this case. It is a slot machine lottery program. Through MVVM + CRDataBind chain response programming, it quickly completes multiple data binding services with interface interaction.

1. demo effect

The main data binding chains are:

  • Model.sn < --- > snlb.text < --- > self.view.backgroundcolor
  • Model.winrate < --- > ratelb.text < --- > rateslider.value (sliding slider changes the winning rate)
  • Model.code < --- > codelb.text (the lottery number changes after the lottery)
  • Model.wincode < --- > wincodelb.text < --- > wincodetf.text (set the winning number of the next issue)
  • Model.iswin < --- > iswinlb.text < --- > self.iswin (display and release winning, play digital flashing animation)

2. Effect description

For example, in the demo, when you need to configure the winning number of the next slot machine, the business code before CRDataBind is written as follows:

- (void)setupBind {
    // Bind textField edit event
    [self.winCodeTF addTarget:self    action:@selector(winCodeTFdidEdittingChanged:) forControlEvents:UIControlEventEditingChanged];
​
    // Call from unknown place
    self.lotteryVM.currentLottery.winCode = @"222";
    [self freshWinCodeUI];
}
​
- (void)winCodeTFdidEdittingChanged:(UITextField *)textField {
    if (textField.text.length > 3) {
        textField.text = [textField.text substringToIndex:3];
        return;
    }
    self.lotteryVM.currentLottery.winCode = self.winCodeLb.text = textField.text;
}
​
- (void)freshWinCodeUI {
    // Refresh interface
    NSString *winCode = self.lotteryVM.currentLottery.winCode;
    self.winCodeLb.text = self.winCodeTF.text = winCode;
Copy code

It can be seen that the above is scattered and cumbersome. Let's see if it becomes much cleaner and refreshing when we use CRDataBind:

- (void)setupBind {
    /**binding
     model.winCode <---> winCodeLb.text <---> winCodeTF.text
     Add filter to filter the response with the winning number greater than 3 digits
     */
    CRDataBind
    ._inout(self.lotteryVM.currentLottery, @"winCode")
    ._out(self.winCodeLb, @"text")
    ._inout_ui(self.winCodeTF, @"text", UIControlEventEditingChanged)
    ._filter(^BOOL(NSString *text) {
        // Filtering: the winning number must be less than 3 digits
        return text.length <= 3;
    });
}
Copy code

6, Core code scope

The code is located in the directory CRDataBindDemo/CRDataBindDemo/CRDataBind /

---CRDataBind

+---CRDataBindDefine.h

+---CRDataBind.h

+---CRDataBind.m

+---CRDataBindObserverManager.h

+---CRDataBindObserverManager.m

+---CRDataBindObserver.h

+---CRDataBindObserver.m

+---NSObject+DataBind.h

+---NSObject+DataBind.m

+---CRDataBindObserverModel.h

+---CRDataBindObserverModel.m

+---CRDataBindTargetModel.h

+---CRDataBindTargetModel.m

Posted on Wed, 24 Nov 2021 01:08:47 -0500 by tieded