# introduction

As I said in the previous article, I was recently taught that no MVVM architecture is complete without bidirectional data binding. Therefore, the MVVM mentioned in this article only represents the MVVM architecture in iOS as understood by the table person. Please bear with me if there is any mistake (your boss can not hit me 🤪).

# Personal understanding of MVVM

When writing another article before (2020), I just learned to use RxSwift and found that there are different ways to use the ViewModel layer. Common ones are:

  1. External parameters are required to create a ViewModel. The ViewModel is used to convert an input to an output.

  2. The initialization of the ViewModel does not depend on external parameters; The ViewModel provides function calls externally and internally converts the calls to the output of an Observable

    .

The pros and cons of the two methods will not be evaluated here. I used the second method in the actual project and used the protocol to distinguish Input and Output, as shown in the following example:

protocol DynamicListViewModelInputs {

    func viewDidLoad(a)
    func refreshDate(a)
    func moreData(with cursor: String.needHot: Bool)
}

protocol DynamicListViewModelOutputs {

    var refreshData: Observable<DynamicDisplayModel> { get }
    var moreData: Observable<DynamicDisplayModel> { get }
    var endRefresh: Observable<Void> { get }
    var hasMoreData: Observable<Bool> { get }
    var showError: Observable<String> { get}}protocol DynamicListViewModelType {
    var input: DynamicListViewModelInputs { get }
    var output: DynamicListViewModelOutputs { get}}final class DynamicListViewModel: DynamicListViewModelType.DynamicListViewModelInputs.DynamicListViewModelOutputs {
.
.
}
Copy the code

External use:

/ / create the VM
private let viewModel: DynamicListViewModelType = DynamicListViewModel(a)private let dataSource = DynamicListDataSource(a).

override func viewDidLoad(a) {
    super.viewDidLoad()
    .

    / / call the input
    viewModel.input.viewDidLoad()
}
.

func bindViewModel(a) {

    / / to subscribe to the output
    viewModel.output.refreshData.subscribe(onNext: { [weak self] wrappedModel in
        self?.dataSource.newData(from: wrappedModel)
        self?.tableNode.reloadData()
    }).disposed(by: disposeBag)
    .
}
Copy the code

In this way, the ViewModel also achieves interface isolation. Similarly, no data and state variables are stored in the ViewModel. For TableNode data, the DataSource is removed for storage, further separating the responsibilities of the ViewModel. The ViewModel only processes the data. The DataSource stores the data and provides the intermediate state.

The above MVVM adds types, but it improves code readability and maintainability.

# Correction to use of RxSwift

In the original text, we simply used Moya and RxSwift, but the network request and logic processing in the actual project will be more complicated than now, for example, to obtain the data of recommendation circle when reading the data of the home page, to read the data of recommendation boiling point only when loading the data of specific pages (such as the second page, etc.), to add hot topics, etc. The following is only an example to add the function of recommending circles:

  1. Add interface call of recommendation circle and display of related interface.

  2. If the recommendation circle fails to be read, the corresponding view is not displayed.

  3. Recommendation circles do not affect existing functionality.

You can start by thinking about the changes to implement the secondary requirements.

We first revise the unreasonable use of RxSwift in the original text.

# compactMap 和 map

In the original ListViewModel

private let loadDataSubject: BehaviorSubject<String? >= BehaviorSubject(value: nil)
.
let loadDataAction = self.loadDataSubject.filter { $0 ! = nil }.map { string -> String in
    guard let cursor = string else { fatalError("")}return cursor
}
Copy the code

Is amended as:

let loadDataAction = loadDataSubject.compactMap { $0 }
Copy the code

CompactMap itself is used to remove nil which is consistent with Swift.Collection.

# XxxxSubject 和 error

Be careful not to use observer.error (XXX) when using RxSwift, especially in operators such as flatMap. For example, in ListViewModel:

loadDataAction.filter { $0 = = "0" }.map { cursor -> DynamicListParam in
        return DynamicListParam(cursor: cursor)
    }.flatMap { param -> Single<XTListResultModel> in
        / / comment 1
        return DynamicNetworkService.list(param: param.toJsonDict())
        .request()
        .map(XTListResultModel.self)}Copy the code

Note 1: If a network bump causes an error event to be thrown in Rx + Moya, we directly converted our loadDataSubject into a Single

generated for RxMoya. Therefore, an error event causes loadDataSubject to terminate the stream of events and not send a new element. Therefore, a catch error is required for RxMoya, and this error information is needed in the subsequent combination, so the Result is wrapped with Result

, and the code is as follows:
,>

let dynamycData = loadDataAction.filter { $0 ! = "0" }.map { cursor -> DynamicListParam in
    DynamicListParam(cursor: cursor)
}.flatMap { param -> Observable<Result<XTListResultModel.Error>> in
    let result = DynamicNetworkService.list(param: param.toJsonDict())
        .request()
        .map(XTListResultModel.self)
        .map { model -> Result<XTListResultModel.Error> in
                .success(model)
        }.catch { .just(.failure($0))}return result.asObservable()
}
Copy the code

LoadDataSubject will not be terminated if RxMoya receives network requests, Model Decoder, etc.

# Model changes

Now the UI needs to display different types of cells, so we need to wrap the DynamicListModel:

enum DynamicDisplayType {
    case dynamic(DynamicListModel)
    case topicList([TopicModel])
    case hotList([DynamicListModel])}/ / / the corresponding XTListResultModel
struct DynamicDisplayModel {

    var cursor: String? = nil
    var errMsg: String? = nil
    var errNo: Int? = nil
    var displayModels: [DynamicDisplayType] = []
    var hasMore: Bool = false
    var dynamicsCount: Int = 0
    .
}
Copy the code

Add data requests and processing to the VM

With the above adjustment, we can officially implement the recommendation circle function. Define the Observe that requests the boiling point of the home page separately, as shown in the error code above, and add the corresponding PublishSubject in the ListViewModel

// Add circle data request
private let topicListSubject = PublishSubject<Void> ()func loadFirstPageData(a) {
    topicListSubject.onNext(())
    loadDataSubject.onNext("0")}Copy the code

Replace loaddatasubject.onNext (“0”) with loadFirstPageData().

Increase in func initializedNewDateSubject () of a topicListSubject flatMap {}

let topicListData = topicListSubject.flatMap { _ -> Observable<Result<TopicListModel.Error>> in
    let result = DynamicNetworkService.topicListRecommend
        .memoryCacheIn()
        .request()
        .map(TopicListModel.self)
        .flatMap { model -> Single<Result<TopicListModel.Error>> in
            .just(.success(model))
        }.catch {
            .just(.failure($0))}return result.asObservable()
}
Copy the code

Zip topicListData and dynamycData:

let dynamycData = loadDataAction.filter { . }

let topicListData = topicListSubject.flatMap { . }

let newDataSubject = Observable.zip(dynamycData, topicListData).map { (dynamicWrapped, topicListWrapped) -> Result<DynamicDisplayModel.Error> in
    var displayModel = DynamicDisplayModel(a)switch dynamicWrapped {
    case .success(let wrapped):
        displayModel = DynamicDisplayModel.init(from: wrapped)
    case .failure(let error):
        return .failure(error)
    }

    switch topicListWrapped {
    case .success(let wrapped):
        if let list = wrapped.data, !list.isEmpty {
            displayModel.displayModels.insert(.topicList(list), at: 0)}case .failure(let error):
        // FIXED: - Request or parse data failure, no processing, no display interface
        print(error)
    }

    return .success(displayModel)
}
Copy the code

# DataSource adjustment

Now that we’re done with the networking and data processing, let’s change our DataSource. It’s a little easier to change our XTListResultModel to DynamicDisplayModel, Replace DynamicListModel with DynamicDisplayType, then modify the following code in ASTableDatasource:

func tableNode(_ tableNode: ASTableNode.nodeForRowAt indexPath: IndexPath) -> ASCellNode {
    let model = commendList[indexPath.row]

    switch model {
    case .dynamic(let dynModel):
        let cellNode = DynamicListCellNode()
        cellNode.configure(with: dynModel)
        return cellNode
    case .topicList(let topic):
        let cellNoed = DynamicTopicWrapperCellNode()
        cellNoed.configure(with: topic)
        return cellNoed
    case .hotList(let list):
        // TODO:- there needs to be replaced with DynamicHotListWrapperCellNode
        let cellNode = DynamicListCellNode()
        cellNode.configure(with: list[0])
        return cellNode
    }
}
Copy the code

All operations are completed and the function is realized.

Hey!!!!!! I did not forget the ViewController, but it does need to modify place, is the most subsequent add DynamicTopicWrapperCellNode delegate methods.

# added

  • For network layer encapsulation can see the boiling point page imitation of the supplement – network layer, of course, now the source code has included this part of the content.

  • This is not caused by Rx or other third-party libraries. You can simply switch to iOS 10 and reinstall pod Install. Using iOS 13 is just to replace Rx later.