One MVVM + ReactiveCocoa practice, mvvmreactivecocoa

Source: Internet
Author: User

One MVVM + ReactiveCocoa practice, mvvmreactivecocoa
Preface

I have been learning MVVM and ReactiveCocoa (RAC) for a while, but I only want to read blogs. I have been very interested in these two things and think they are very innovative, I have been trying to find a chance to practice it in the project, but I still have some concerns. After all, I have never practiced it, and the online materials are a bit confused, in fact, the hand may still be difficult. So I decided to write a simple demo for practice. I chose an interface in the project I just wrote to implement it. In order to meet the actual project requirements, let's see how to implement it with MVVM + RAC. (I will not talk about the basic introduction of MVVM and ReactiveCocoa here. You can refer to relevant information on the Internet)

Implemented Functions

The implementation of the function is very simple, just a list interface, UITableView done, you can pull down and refresh, pull up to load more. The final effect is as follows:

Project Structure Used

Model: Entity
View: Storyboard, xib, and custom view
ViewController: UIViewController. The Controller corresponding to the interface we want to implement is ProductListViewController.
ViewModel :( how to translate this? View object ?) You know.
API: Network request related

Third-party libraries used:

1 pod 'AFNetworking', '~> 2.5.3'2 pod 'ReactiveCocoa', '~> 2.5'3 pod 'MJRefresh', '~> 2.4.7'4 pod 'MJExtension', '~> 2.5.9'5 pod 'AFNetworking-RACExtensions', '~> 0.1.8'

Besides AFNetworking and ReactiveCocoa, they are two popular class libraries of MJ, which are very common. (Here, let me make a sad look. When I started to write this demo, RAC3.0 was only alpha and beta, so I used version 2.0, a final official version of 2.5, however, when I was writing this article, I ran the pod search and found that the version of 4.0alpha has been released. I don't know what changes have been made in version 4.0, however, I know that in version 3.0, RACCommand is marked as deprecate, which is replaced by RACAction. The usage should be similar)

Implementation Details (combining MVVM and ReactiveCocoa)

 

Get List Data

 

We all know that in MVVM, network communication-related operations should be handled by ViewModel. Therefore, a RACCommand is defined in ProductListViewModel, which is called:

1/** 2 * Get Data Command3 */4 @ property (nonatomic, strong, readonly) RACCommand * fetchProductCommand;

Initialize the ViewModel In the init method:

1 _fetchProductCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {2         3         return [[[APIClient sharedClient]4                  fetchProductWithPageIndex:@(1)]5                  takeUntil:self.cancelCommand.executionSignals];6     }];

Subscribe to RACCommand and assign the value to items after obtaining the data (items is an array that stores all the data, that is, the dataSource of tableView)

 1    @weakify(self); 2     [[_fetchProductCommand.executionSignals switchToLatest] subscribeNext:^(ResponseData *response) { 3         @strongify(self); 4         if (!response.success) { 5             [self.errors sendNext:response.error]; 6         } 7         else { 8             self.items = [ProductListModel objectArrayWithKeyValuesArray:response.data]; 9             self.page = response.page;10         }11     }];

In ProductListViewController, subscribe to the items of ViewModel and reload tableview when there is a change.

1     [RACObserve(self.viewModel, items) subscribeNext:^(id x) {2         @strongify(self);3         [self.table reloadData];4     }];

The dataSource of tableView is as follows:

 1 #pragma mark - UITableViewDataSource 2  3 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 4     return 1; 5 } 6  7 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 8     return self.viewModel.items.count; 9 }10 11 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {12     ProductListCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ProductListCell" forIndexPath:indexPath];13     cell.viewModel = [self.viewModel itemViewModelForIndex:indexPath.row];14     15     return cell;16 }

Let's look at the custom tableViewCell:

 1 - (id)initWithCoder:(NSCoder *)aDecoder { 2     self = [super initWithCoder:aDecoder]; 3      4     if (self) { 5         @weakify(self); 6         [RACObserve(self, viewModel) subscribeNext:^(id x) { 7              8             @strongify(self); 9             self.productNameLabel.text = self.viewModel.ProductName;10             self.bankNameLabel.text = self.viewModel.ProductBank;11             self.profitLabel.text = self.viewModel.ProductProfit;12             self.saleStatusLabel.text = self.viewModel.SaleStatusCn;13             self.productTermLabel.text = self.viewModel.ProductTerm;14             self.productAmtLabel.text = self.viewModel.ProductAmt;15             16         }];17     }18     19     return self;20 }

It is so convenient to have RAC, so do not block the callback, and do not need to delegate.

Get more data

More resources are loaded on the server. MJ has helped us solve the problem. We only need to define a RACCommand to load more data in ViewModel for calling. I will not introduce it here. For details, refer to the final demo.

UITableView refresh status Switch

Anyone who has used MJRefresh knows that, whether it is header or footer or beginRefreshing, endRefreshing must be called to switch the refresh status after obtaining the data. With RAC, We can subscribe to the executing signal of RACCommand, as shown below:

1     @weakify(self)2     [_viewModel.fetchProductCommand.executing subscribeNext:^(NSNumber *executing) {3         NSLog(@"command executing:%@", executing);4         if (!executing.boolValue) {5             @strongify(self)6             [self.table.header endRefreshing];7         }8     }];

The above is almost the logic interaction between ViewModel and ViewController. They are connected through the ReactiveCocoa bridge.

AFNetworking is familiar with http requests. AFNetworking-RACExtensions converts the http requests in AFNetworking to RACSignal. In the ReactiveCocoa world, everything is Signal (not sure whether it is correct or not ).

I encapsulated an httpGet method:

1-(RACSignal *) httpGet :( NSString *) URLString parameters :( id) parameters {2 return [[[self rac_GET: URLString parameters: parameters] 3 catch: ^ RACSignal * (NSError * error) {4 // handle Error 5 NSLog (@ "error: % @", error); 6 // TODO: here we can refer to error. code to determine the network exception, and give different error prompts 7 return [RACSignal ERROR: [NSError errorWithDomain: @ "error" code: error. code userInfo: @ {@ "Success": @ NO, @ "Message": @ "Bad Network! "}]; 8}] 9 performanceeach: ^ id (id responseObject, NSURLResponse * response) {10 NSLog (@" url: % @, resp: % @ ", response. URL. absoluteString, responseObject); 11 ResponseData * data = [ResponseData objectWithKeyValues: responseObject]; 12 13 return data; 14}]; 15}

There are two main tasks in this section. The first is error processing (as described below), and the second is parsing the returned data. json data is generally converted into a Model.

In actual projects, basically all api interface return value formats are unified (if they are not uniform, you can call the provider), So I defined a Model called ResponseData, this Model has an NSObject type attribute to receive different types of values (arrays, objects (I .e. dictionaries ). In this way, each api interface can convert the format of the NSObject type attribute based on the actual situation, which is convenient to use.

Error Handling

The error handling can be divided into several situations, such:
1) network errors (no network, timeout, etc)
2) server errors (404, 500, and so on)
3) Incorrect business logic
The first two errors will go to the errors signal channel of RACCommand. We can see in the encapsulated httpGet method above that we caught error, then we can identify the error based on the error code. The purpose of this distinction is to show users different error prompts, which is more friendly.
The third type of "error" is actually a normal json string returned by the server. We will parse it into a ResponseData object. At this time, we have to determine whether an error has occurred. For two different situations, if you want to separate them, there will inevitably be a lot of repeated code. As a programmer pursuing high-quality code, this is an undesirable solution (or even intolerable ). My solution is (refer to pipeline ):

1) define a BaseViewModel as the base class of all viewmodels.

1 @ interface BaseViewModel: NSObject 2 3 @ property (nonatomic) RACSubject * errors; 4 5/** 6 * cancel Request Command 7 */8 @ property (nonatomic, strong, readonly) RACCommand * cancelCommand; 9 10 @ end

2) Merge the errors of RACCommand:

1 [[RACSignal merge:@[_fetchProductCommand.errors, self.fetchMoreProductCommand.errors]] subscribe:self.errors];

3) In the RACCommand subscription, check whether there is an error. If there is an error, manually send an error.

 1   @weakify(self); 2     [[_fetchProductCommand.executionSignals switchToLatest] subscribeNext:^(ResponseData *response) { 3         @strongify(self); 4         if (!response.success) { 5             [self.errors sendNext:response.error]; 6         } 7         else { 8             self.items = [ProductListModel objectArrayWithKeyValuesArray:response.data]; 9             self.page = response.page;10         }11     }];

4) Subscription to errors in ViewModel in ViewController.

1 [_ viewModel. errors subscribeNext: ^ (NSError * error) {2 ResponseData * data = [ResponseData objectWithKeyValues: error. userInfo]; 3 NSLog (@ "something error: % @", data. keyValues); 4 // TODO: here you can select an appropriate method to display the error message 5}];

The principle is to unify all errors into one channel, so you only need to process them in one place.

Http request cancel

When implementing some interface functions, we usually make http requests after the interface is opened. Sometimes an indicator is displayed to tell the user that the user is requesting data. However, if the network is poor (for example, 2G network), sometimes the user may think that the wait time is too long, and then click back. Although the interface is closed, but for the http request, it is still ongoing. At this time, the better way to handle this is to drop the http request cancel. Without RAC, we need to record the NSURLSessionTask of each http Request (if you use AFNetworking's AFHTTPSessionManager ), then, call [task cancel] In the dealloc of Viewcontroller to cancel the task. When the task is cancel, an error is returned, in this case, you need to determine whether the errorCode is cancel to avoid confusion with other network exceptions.
How can we implement http cancel with ReactiveCocoa? Fortunately, AFNetworking-RACExtensions has been encapsulated for us. We only need to define a RACCommand in ViewModel to cancel the http request (which can be placed in BaseViewModel ), then, call this command as necessary. Of course, the premise is that the following code is set in the command that initiates the http request:

1 _fetchProductCommand = [[RACCommand alloc]initWithSignalBlock:^RACSignal *(id input) {2         3         return [[[APIClient sharedClient]4                  fetchProductWithPageIndex:@(1)]5                  takeUntil:self.cancelCommand.executionSignals];6     }];

The core point is takeUntil, which indicates "always execute ...", Here, the http request is executed until the cancel command is issued. After testing, we can find that our goal can be fully achieved.
PS: here we will explain how to simulate an unstable network. Set-> developer-> network link conditioner, which has various options to choose from, such as 100% Loss, 3G, and Very Bad Network. Although not as powerful as professional tools, however, it is sufficient to simulate an abnormal network.

Definitions of Model and ViewModel

The relationship between the two is clear and unclear.

Why is it clear? Because Model is an object, it is generally a property field, and ViewModel is a bridge between ViewController and Model. ViewModel contains RACCommand and some business logic (such as paging processing, viewController only needs to call fetchData or fetchMoreData, and does not need to know what page is displayed ).

Why is it unclear? In my demo, there is a ViewModel (ProductListCellViewModel) for customizing tablecell, which is actually some attributes, basically the same as ProductListModel. In this case, I am confused. Is Model used as ViewModel, or is it necessary to separate redundant code? In addition, the data returned by the http request is generally the data that the ViewController needs to display (only in general, but also needs additional processing ).

What should I do? My understanding:
1) the data obtained from the http request is sourceData, and our Model exists as sourceData. Therefore, I prefer to map json data with Model.
2) ViewModel is used to obtain the Model for processing (sometimes no additional processing is required) and then provided to ViewController for use. For example, it is directly displayed on The View.

This is also the core of the MVVM framework. Therefore, the items in ViewModel stores the array of Model. The problem arises again. Since Model is in items, and ViewController obtains sourceData through ViewModel, where can we convert Model to ViewModel?

I can think of three solutions:
1) After json data is parsed using the Model, the cyclically traversing Model is converted to ViewModel and saved to items. In this way, ViewModel rather than Model is stored in items. When using TableCell, you can directly use ViewModel in items.
2) items saves the Model and TableCell uses the Model directly. This may happen when the Model and ViewModel are almost identical. I think it is not worthwhile to completely copy a ViewModel, but it is not in line with MVVM.
3) items stores the Model. When TableCell obtains the ViewModel, it initializes the ViewModel through the Model.
I currently use 3rd solutions. In ViewModel, Model is used as an attribute, and some readonly attributes are provided and its get method is rewritten (data can be formatted in the middle) for use on the interface.

Pitfalls

It is difficult to learn RAC independently. After all, it is difficult to fully understand the APIs of RAC. In addition, when you are not familiar with it at the beginning, it is difficult to come up with a more reasonable RAC Processing Method for certain specific scenarios (this sentence is a theft of others, but I also have a deep understanding ).

Here are some of the pitfalls I encountered when writing this demo. It is also a merit to help others bypass these pitfalls.
1) The array used to save data in ViewModel. NSMutableArray cannot be used. The reason is that RAC is based on KVO, while the Add and Remove methods of NSMutableArray do not send notifications to KVO. Therefore, RACObserve on NSMutableArray will not achieve the desired results. (Similarly, other Mutable instances cannot be used)
2) When you assign values to items in ViewModel, you cannot use _ items = somearray, but self. items. In the beginning, I want to define a readonly items attribute in viewmodel (in theory, it should also be readonly, because ViewController is only responsible for retrieving data from ViewModel), and then assign values through _ items, however, messages cannot be received after you subscribe to viewmodel items. I have always felt that this was not scientific. Maybe it was wrong to open it, but it was not solved in the end. I hope you will be grateful to anyone who knows this.
3) replay, replayLast, and replayLazily cannot be used for cancel-enabled http requests. For the distinction between the three, I think the analysis is very detailed.

Summary

The above is my MVVM + RAC practice. I am a beginner in MVVM and RAC. It is inevitable that some concepts and understandings are biased. You are welcome to criticize and correct them. You are also welcome to discuss them together. In order to better learn and make progress!

Here is my demo source code: Portal

(Because the demo uses the actual project interface, I can erase it)

 

Related Article

Contact Us

The content source of this page is from Internet, which doesn't represent Alibaba Cloud's opinion; products and services mentioned on that page don't have any relationship with Alibaba Cloud. If the content of the page makes you feel confusing, please write us an email, we will handle the problem within 5 days after receiving your email.

If you find any instances of plagiarism from the community, please send an email to: info-contact@alibabacloud.com and provide relevant evidence. A staff member will contact you within 5 working days.

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.