Understanding RxSwift: Why use RxSwift (part 1)
Understanding RxSwift: Implementation Principles (part 2)
RxSwift: MVVM (3)
Understanding RxSwift: Unit Testing (Part 4)
When our APP has a complex interaction and logic, the ViewController becomes bloated, with a lot of code filling it, giving it too much responsibility. Bloated ViewControllers are difficult to understand, maintain, and extend, adding complexity to subsequent development and reducing overall development efficiency.
The current popular solution is the MVVM architecture, which introduces the ViewModel on the basis of MVC, and moves data transformation operations such as data presentation and style customization into it. So the ViewController only needs to be responsible for the data deployment, so it has less responsibility, and the bloat situation automatically disappears.
RxSwift provides a binding mechanism that automatically synchronizes data with views and notifies the other party of changes, eliminating the need to write a lot of cumbersome imperative binding code.
Let’s write a demo to see how RxSwift combined with MVVM slimmed down the ViewController.
Source code address: github.com/superzcj/Rx…
Demo
This is a page to add Chinese herbal medicine, each medicine has the quantity and price, at the bottom of the summary of the selected medicine type, quantity and total price. Users can add, subtract or delete drugs. Each action will refresh the bottom summary price information.
When entering for the first time, load the default drug list from the back end, change any drug, and automatically synchronize the bottom summary price information.
To prepare
First, add RxSwift to the project. We typically use Cocoapods to manage third-party dependent libraries. In the POD file, add the following code:
pod 'RxSwift'
pod 'RxCocoa'
pod 'RxDataSources'
Copy the code
RxCocoa is part of RxSwift and is primarily the Rx encapsulation of UI-related controls, such as the binding capabilities of controls. RxDataSources includes binding capabilities associated with UITableView and UICollectionView.
ViewModel
The ViewModel transforms the input data into another data. In this demo, the default list interface is loaded from the back end when the page opens, which we can use as a reload event to fire when viewDidLoad is executed; When clicking the Increase or decrease button on the cell to change the number of selected drugs, we can also act as an event editTrigger; When clicking the Delete button on the cell removes the drug, the deleteTrigger indicates this action. Finally, the input data consists of three events: Reload, editTrigger, and deleteTrigger.
All three events are of type PublishSubject and act as a listening sequence that responds when an event is triggered.
The output has two pieces of data, items representing the list of medicines and totalCount representing the summary information to be displayed.
The BehaviorRelay is a sequence that has a value property that gets the latest value. Its Accept () method lets you modify the value and send it out. Driver is a special sequence for simplified UI layer code that does not generate error events and must be listened on MainScheduler.
struct Input {
let reload: PublishSubject<[DrugCellViewModel]>
let editTrigger: PublishSubject<DrugCellViewModel>
let deleteTrigger: PublishSubject<DrugCellViewModel>
}
struct Output {
let items: BehaviorRelay<[DrugCellViewModel]>
let totalCount: Driver<String>
}
Copy the code
The Transform method converts input to output. First, a sequence of BehaviorRelay type is defined to store the drug list data. When the input.reload event is triggered, the backend requests the interface, gets the data, and passes it to Elements. When the input.editTrigger operation is triggered, the drug list data is regenerated, and the matched model is replaced according to the cell model brought in to get the new drug list data. When the input.deleteTrigger operation is triggered, the imported cell model is removed and new drug list data is generated.
The totalCount is again based on elements, iterating through elements to find the type, quantity, and unit price of the drug to calculate the final summary price data.
func transform(input: Input) -> Output {
let elements = BehaviorRelay<[DrugCellViewModel]>(value: [])
input.reload.flatMapLatest({ (item) -> Observable<[DrugCellViewModel]> in
return self.request()
}).subscribe(onNext: { (items) in
elements.accept(items)
}).disposed(by: self.disposeBag)
input.editTrigger.subscribe(onNext: { (item) in
var arr = [DrugCellViewModel]()
for model in elements.value {
if model.drugId == item.drugId {
arr.append(item)
} else {
arr.append(model)
}
}
elements.accept(arr)
}).disposed(by: self.disposeBag)
input.deleteTrigger.subscribe(onNext: { (item) in
if let index = elements.value.firstIndex(of:item) {
var arr = elements.value
arr.remove(at:index)
elements.accept(arr)
}
}).disposed(by: self.disposeBag)
let totalCount = elements.map({ (items) -> String in
var sum = 0;
var priceSum = 0.00;
for cellViewModel in items {
sum += cellViewModel.drugCount
let price : Double = (cellViewModel.maxPrice) * Double(cellViewModel.drugCount)
priceSum += price
}
return "\(items. Count), \(sum)g, \(priceSum) Yuan"
}).asDriver(onErrorJustReturn: "")
return Output(items: elements, totalCount: totalCount)
}
Copy the code
ViewController
In the ViewController, we bind the tableView data source using RxSwift.
Start by defining a variable DrugViewModel and, in the viewDidLoad method, initialize the view and data binding
var viewModel: DrugViewModel = DrugViewModel()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
configView()
setupViewModel()
reloadSubject.onNext([])
}
Copy the code
According to the user’s three action events: reloadSubject, editTrigger and deleteTrigger, the output data output is obtained. We then bind output.items to the tableView and output.totalCount to the bottom label.
When configuring the drug cell, the cell has two callbacks, increase or decrease quantity callbacks and delete callbacks. When these two callbacks are triggered, editSubject and deleteTrigger operation signals are sent to the viewModle respectively.
func setupViewModel() {
let input = DrugViewModel.Input(reload: reloadSubject, editTrigger: editSubject, deleteTrigger: deleteTrigger )
let output = viewModel.transform(input: input)
output.totalCount.drive(onNext: { (content) in
self.titleLabel.text = content
}).disposed(by: disposeBag)
output.items.map({ (items) -> [DrugCellViewModel] in
self.cellViewModels = items
return items
}).asDriver(onErrorJustReturn: []).drive(tableView.rx.items(cellIdentifier: CellIdentifiers.DrugTableViewCell, cellType: DrugTableViewCell.self)) { index, viewModel, cell in
let rowViewModel = self.cellViewModels[index]
cell.setup(viewModel: rowViewModel)
rowViewModel.numberButtonTapped.asObserver().subscribe(onNext: { (drugCellViewModel) in
self.editSubject.onNext(drugCellViewModel)
}).disposed(by: self.disposeBag)
rowViewModel.deleteButtonTapped.asObserver().subscribe(onNext: { (drugCellViewModel) in
self.deleteTrigger.onNext(drugCellViewModel)
}).disposed(by: self.disposeBag)
}.disposed(by: self.disposeBag)
}
Copy the code
CellViewModel and Cell
For the Cell in the drug list, it holds a property DrugCellViewModel that binds the cellViewModel to the View at initialization
private var viewModel: DrugCellViewModel?
public func setup(viewModel:DrugCellViewModel?) {
self.viewModel = viewModel
configureView()
}
private func configureView() {
guard let viewModel = viewModel else { return }
drugNameLabel.text = viewModel.drugName
textField.text = "\(viewModel.drugCount)"
drugPriceLabel.text = "\ [the viewModel. MaxPrice) yuan"
}
Copy the code
The DrugCellViewModel has a DrugModel corresponding to this Cell, which modifies the DrugModel and passes the callback event when it receives a user click event.
NumberButtonTapped and deleteButtonTapped are two callback events that are triggered when a button on the Cell is clicked, with arguments of their own.
class DrugCellViewModel: NSObject {
var drugModel: DrugModel
var drugCount:Int
var numberButtonTapped = PublishSubject<DrugCellViewModel>()
var deleteButtonTapped = PublishSubject<DrugCellViewModel>()
init(drugModel:DrugModel) {
self.drugModel = drugModel
drugCount = drugModel.drugCount
}
func addDrug() {
self.drugModel.drugCount += 1
drugCount+=1
numberButtonTapped.onNext(self)
}
func minusDrug() {
if drugCount > 0 {
drugCount-=1
self.drugModel.drugCount -= 1
}
numberButtonTapped.onNext(self)
}
func deleteDrug(){
deleteButtonTapped.onNext(self)
}
}
Copy the code