preface

My previous understanding and use of MVVM felt shallow, and was only useful in projects to lighten the load on ViewController

  1. There is no binding between data and View, no true data-driven View
  2. Does not take advantage of the ease of testing of MVVM
  3. The use of RxSwift was also limited to the network request library, and some of the benefits of RxCocoa did not apply to the project

So it was time to use the real MVVM in the project (sort out the routine code). Since RxSwift was introduced in the project, it was implemented, and some familiarity with and use of RxSwift may be required before you follow this article.

The MVVM architecture diagram

MVVM directory structure

The image above shows a module in the project, using the file structure of the MVVM architecture. The Model is centrally defined in a common folder, which I will discuss in detail next.

ViewModel

Looked up a lot of information, different people have many kinds of ViewModel implementation, I summarized here most people is I agree with a method of implementation

class ViewModel {
    // Input to output, this is the real business logic code
    func transform(input: Input) -> Output{}}extension ViewModel {
    // The type is Driver, as it relates to UI controls
    struct Input {}// The output type is also a Driver
    struct Output {}}Copy the code

Model

For models, it’s about defining data models, but you can also encapsulate common business methods such as data transformations.

Viewcontrollers and View

The main function of the ViewController is to manage the life cycle of the View and bind the relationship between the data and the View. Data binding is mainly implemented through RxDataSources+RxSwift, so you should introduce these two libraries in your project. RxCocoa provides Rx support for UI frameworks that allow us to use button click sequences so that we can provide input to the ViewModel, and RxDataSources helps you simplify the process of writing data sources for TabelView or CollectionView, And provides a way to update the TableView through a sequence, at this time we just need to bind the ViewModel data output sequence to the TableView data source sequence.

Navigator

The Navigator is stripped out of the ViewController to control the view jump

In the code

The following image shows a page in the directory structure above

Let’s analyze the inputs and outputs on the lower interface

Input: request when entering the page, rename button click, delete button click, new group button click

Output: TableView data source, page Loading state

ViewModel core code:

class MenuSubGroupViewModel {
    func transform(input: Input) -> Output {
        let loadingTracker = ActivityIndicator(a)let createNewGroup = input.createNewGroup
            .flatMapLatest { _ in
                self.navigator.toMenuEditGroupVC()
                    .saveData
                    .asDriverOnErrorJustComplete()
            }
        let renameGroup = input.cellRenameButtonTap
            .flatMapLatest...
        let getMenusInfo = Driver.merge(createNewGroup, input.viewDidLoad, renameGroup)
            .flatMapLatest...
        let deleteSubGroups = input.cellDeleteButtonTap
            .flatMapLatest...
        let dataSource = Driver.merge(getMenusInfo, deleteSubGroups)
        let loading = loadingTracker.asDriver()
        return Output(dataSource: dataSource, loading: loading)
    }
}
extension MenuSubGroupViewModel {
    struct Input {
        let createNewGroup: Driver<Void>
        let viewDidLoad: Driver<Void>
        let cellDeleteButtonTap: Driver<IndexPath>
        let cellRenameButtonTap: Driver<IndexPath>}struct Output {
        let dataSource: Driver"[MenuSubGroupViewController.CellSectionModel] >let loading: Driver<Bool>}}Copy the code

Now, one might wonder why we’re saving the page’s data, but isn’t our data bound to the TableView directly through a web request that generates a sequence? Because we need to save it in some business scenarios, such as the error in the network request, I hope have before page will continue to show the state of the data, then we can be in the wrong sequence network request into before we save the data, so the page or display the same, and you didn’t note this is private property. ActivityIndicator: Can monitor the status of network requests to change the loading state. Specific implementation is posted in the code below.

CreateNewGroup: Clicking the New group button on the page sends a sequence as ViewModel input. The new group is created on the next page through the flatMapLatest conversion operation and the result is returned as a sequence. SaveData here is a PublishSubject type that can receive or send sequences because the Driver can only receive and not send them. If successful, refresh the page.

ViewDidLoad: When the ViewController calls the viewDidLoad method it sends a sequence as ViewModel input and updates the TableView by transforming the dataSource output.

CellDeleteButtonTap and cellRenameButtonTap: Clicking the button in the cell emits a sequence as ViewModel input, then executes the corresponding business code, and finally generates output.

DataSource: the dataSource sequence changes to refresh the TableView.

Loading: Sequence that controls the loading state of the page

ActivityIndicator core code

public class ActivityIndicator: SharedSequenceConvertibleType {
    fileprivate func trackActivityOfObservable<O: ObservableConvertibleType>(_ source: O) -> Observable<O.E> {
        return Observable.using({ () -> ActivityToken<O.E> in
            self.increment()
            return ActivityToken(source: source.asObservable(), disposeAction: self.decrement)
        }) { activity in
            return activity.asObservable()
        }
    }
    private func increment(a) {
        lock.lock()
        value += 1
        subject.onNext(value)
        lock.unlock()
    }
    private func decrement(a) {
        lock.lock()
        value -= 1
        subject.onNext(value)
        lock.unlock()
    }
}
Copy the code

Core code in ViewController

import UIKit
class MenuSubGroupViewController: UIViewController {
    private let cellDeleteButtonTap = PublishSubject<IndexPath> ()// Delete the grouping sequence. When the delete button in the cell is clicked, call onNext to send the sequence
    private let cellRenameButtonTap = PublishSubject<IndexPath> ()// Group rename sequences. Call onNext when the rename button is clicked in the cell to send the sequence

    // Initialize the input sequence of ViewModel and bind the output sequence of ViewModel to the View
    func bindViewModel(a) {
        let viewDidLoad = Driver<Void>.just(())
        let input = MenuSubGroupViewModel.Input(createNewGroup: createGroupButton.rx.tap.asDriver(),
                                                viewDidLoad: viewDidLoad,
                                                cellDeleteButtonTap: cellDeleteButtonTap.asDriverOnErrorJustComplete(),
                                                cellRenameButtonTap: cellRenameButtonTap.asDriverOnErrorJustComplete())
        
        let output = viewModel.transform(input: input)
        output.loading..
        output.dataSource
            .drive(tableView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)
    }
  
    private lazy var dataSource: RxTableViewSectionedReloadDataSource<CellSectionModel> = {
        return RxTableViewSectionedReloadDataSource<CellSectionModel>(configureCell: { [weak self] (_, tableView, indexPath, item) -> UITableViewCell in
            let cell: LabelButtonCell = tableView.dequeueReusableCell(LabelButtonCell.self)... cell.rightButton1.rx.tap .subscribe(onNext: { [weak self] (_) in
                    self? .cellDeleteButtonTap.onNext(indexPath) }) .disposed(by: cell.disposeBag) cell.rightButton2.rx.tap...return cell
        })
    }()
}
Copy the code

RxDataSources will not be used in detail here, so we will focus on the bindViewModel method, which defines the input of the page, and obtains the sequence of output through the transform method, and then binds the data source of the TableView. RxCocoa provides Rx calls to many of the system’s basic controls, making it easy to bind data.

Core code in Navigator

class MenuSubGroupNavigator: BaseNavigator {
    func toMenuEditGroupVC(menuUid: String, dishGroupsInfo: DishGroupInfo? = nil) -> MenuEditGroupViewController {
        let navigator = MenuEditGroupNavigator(navigationController: navigationController)
        let viewModel = MenuEditGroupViewModel(navigator: navigator)
        let vc = MenuEditGroupViewController() vc.viewModel = viewModel navigationController? .pushViewController(vc, animated:true)
        return vc
    }
}
Copy the code

conclusion

  1. To build a MVVM project, RxSwift, RxDataSources, Moya are essential, and you need to be able to create a UITableView data source using RxDataSource and have some knowledge of RxSwift.
  2. Click events in the cell are handled in the project by creating a sequence of PublishSubjects in the ViewController and then actively calling the onNext method at the event callback or listener.
  3. For page loading, no data, no network and other states can encapsulate the Rx attribute of ViewController respectively. ActivityIndicator can monitor the status of network request and send sequence to change the page state.
  4. Much of the MVVM project described above is done through sequences, which can be difficult to locate when errors occur.

Source code address, but you can refer to CleanArchitecture XSwift on GitHub.

The copyright of this article belongs to Zaihui RESEARCH and development team, welcome to reprint, reprint please reserve source. @xqqlv