RxSwift + MVVM 构建ViewModel

一、Swift中的协议

协议的介绍
协议的定义方式
protocol CustomProtocol {
    // 这里是协议的定义部分
}
协议的属性

协议可以要求遵循协议的类型提供特定名称和类型的实例属性类型属性协议不指定属性是存储属性还是计算属性,它只指定属性的名称和类型

1.协议要求一个属性必须明确可读的/可读可写的,类型声明后加上 { set get }来表示属性是可读可写的;

2.属性要求定义为变量类型,即使用var而不是let。

protocol CustomProtocol {
    var enableReadOrWritetable: Int { get set }          //可读可写
    var enbaleOnlyReadtable: Int { get }     //可读
}
协议的方法

1.自定义类型遵循某个协议,在定义类型时,需要在类型名称后加上协议名称,中间以冒号(:)分隔。遵循多个协议时,各协议之间用逗号(,)分隔。
2.若是一个类拥有父类,应该将父类名放在遵循的协议名之前,以逗号分隔。

3.在协议中定义方法,只需要定义当前方法的名称、参数列表和返回值类遵循了协议,必须实现协议中的方法。

class Person: CustomProtocol{
    func eating() {
        print("Person eating")
    }
    
    static func doSomething() {
        print("Person teach")
    }
}
var t = Person()
p.eating()
Person.doSomething()


4.协议中也可以定义初始化方法,当实现初始化器时,必须使用required关键字。

protocol CustomProtocol {
    init(age: Int)
}
class Person: CustomProtocol {
    var age: Int
    required init(age: Int) {
        self.age = age
    }
}


5.如果一个协议只能被类实现,需要协议继承自AnyObject

6.如果一个协议协议继承自AnyObject,结构体遵守该协议时,会报错。

注意:实现协议中的 mutating 方法时,若是类类型,则不用写mutating关键字。而对于结构体和枚举,则必须写mutating关键字。

swift 的结构体和枚举中,mutating 在方法中改变方法所属的实例。

protocol Togglable {
    mutating func toggle()
}

enum OnOffSwitch: Togglable {
    case off, on
    mutating func toggle() {
        switch self {
        case .off:
            self = .on
        case .on:
            self = .off
        }
    }
}
var lightSwitch = OnOffSwitch.off
lightSwitch.toggle()
协议作为类型

协议可以被当做一个功能完备的类型来使用,将协议作为类型,主要有以下3种情况👇

  1. 作为函数、方法或者初始化程序中的参数类型或者返回值
  2. 作为常量、变量或属性类型
  3. 作为数组、字典或者其他容器中元素Item的类型

事例:用集成实现圆和长方体的面积

一、用继承的方式

class Shape{
    var area: Double{
        get{
            return 0
        }
    }
}
class Circle: Shape{
    var radius: Double
   
    init(_ radius: Double) {
        self.radius = radius
    }
    
    override var area: Double{
        get{
            return radius * radius * 3.14
        }
    }
}
class Rectangle: Shape{
    var width, height: Double
    init(_ width: Double, _ height: Double) {
        self.width = width
        self.height = height
    }
    
    override var area: Double{
        get{
            return width * height
        }
    }
}
 
var circle: Circle = Circle.init(10.0)
var rectangle: Rectangle = Rectangle.init(10.0, 20.0)
 
var shapes: [Shape] = [circle, rectangle]
for shape in shapes{
    print(shape.area)
}

二、用协议的方法

protocol Shape {
    var area: Double {get}
}
class Circle: Shape{
    var radius: Double

    init(_ radius: Double) {
        self.radius = radius
    }

    var area: Double{
        get{
            return radius * radius * 3.14
        }
    }
}
class Rectangle: Shape{
    var width, height: Int
    init(_ width: Int, _ height: Int) {
        self.width = width
        self.height = height
    }

    var area: Double{
        get{
            return Double(width * height)
        }
    }
}

var circle: Shape = Circle.init(10.0)
var rectangle: Shape = Rectangle.init(10, 20)
//所谓的多态:根据具体的类来决定调度的方法
var shapes: [Shape] = [circle, rectangle]
//这里能区分不同area的原因是因为 在protocol中存放了pwt(协议目录表),可以根据这个表来正确调用对应的实现方法(pwt中也是通过class_method查找,
//同时在运行过程中也记录了metadata,在pwt中通过metadata查找V-Table,从而完成当前方法的调用)
for shape in shapes{
    print(shape.area)
}
//打印结果:314.0    200.0
协议在底层的存储结构

1.前24个字节,主要用于存储遵循了协议class/struct属性值,如果24字节不够存储,会在堆区开辟一个内存空间,然后在24字节中的前8个字节存储该堆区地址(超出24字节是直接分配堆区空间,然后存储值,并不是先存储值,然后发现不够再分配堆区空间)

2.后16个字节分别用于存储vwt(值目录表)、pwt(协议目录表)

二、看RxCocoa做了什么

为了帮助我们更加简单优雅地实现ViewModel和View的双向绑定,RxCocoa已经帮我们把UIKit框架里常用控件的常用属性都搞成了Observable或Binder、有的属性甚至是Subjects。

//
//  TestMVVMViewController.swift
//  SwiftStudyDemo
//
//  Created by denggaoqiang on 2022/3/13.
//

import UIKit
import RxSwift
import RxCocoa

class TestMVVMViewController: UIViewController {
    
    private lazy var textViewModel: TestMVVMViewModel = {
    
        let model = TestMVVMViewModel()
        return model
        
    }()
    
    private lazy var textView: TestMMVVMTextView = {
        let view = TestMMVVMTextView(viewModel: self.textViewModel)
        view.frame = UIScreen.main.bounds
        return view
        
    }()
    
    private lazy var customView: UIView = {
        let view = UIView.init(frame: CGRectMake(0, 100, 200, 30))
        view.backgroundColor = UIColor.white
        return view
    }()
    
    private lazy var customBtn: UIButton = {
        let view = UIButton.init(type: .custom)
        view.backgroundColor = UIColor.blue
        view.frame = CGRect(x: 100, y: 170, width: UIScreen.main.bounds.width-200, height: 40)
        view.addTarget(self, action:#selector(clickTheButton), for: .touchUpInside)
        return view
    }()
    
    private lazy var customLabel: UILabel = {
        let view = UILabel.init(frame: CGRectMake(0, 250, 200, 30))
        view.backgroundColor = UIColor.white
        return view
    }()
    
    private lazy var customImgView: UIImageView = {
        let view = UIImageView.init(frame: CGRectMake(0, 300, 100, 30))
        view.backgroundColor = UIColor.black
        return view
    }()
    
    private lazy var customRxBtn: UIButton = {
        let view = UIButton.init(type: .custom)
        view.backgroundColor = UIColor.blue
        view.frame = CGRect(x: 100, y: 350, width: UIScreen.main.bounds.width-200, height: 40)
        return view
    }()
    
    private lazy var customRxTextField: UITextField = {
        let view = UITextField.init(frame: CGRectMake(0, 400, 300, 30))
        view.backgroundColor = UIColor.white
        return view
    }()
    
    private lazy var customRxScrollView: UIScrollView = {
        let view = UIScrollView.init(frame: CGRectMake(0, 400, 300, 30))
        view.backgroundColor = UIColor.white
        view.contentSize = CGSizeMake(100, 1000)
        return view
    }()
    
    
    let bag = DisposeBag()
    
    var customColor:UIColor = UIColor.blue
    
    override func viewDidLoad() {
        
        super.viewDidLoad()
        
        view.backgroundColor = UIColor.white
//        view.addSubview(customView)
//        view.addSubview(customBtn)
//        view.addSubview(customLabel)
//        view.addSubview(customImgView)
//        view.addSubview(customRxBtn)
//        view.addSubview(customRxTextField)
        view.addSubview(customRxScrollView)
        
//        rxSwift_UIView()
//        rxSwift_UILabel()
//        rxSwift_UIImageView()
//        rxSwift_UIButton()
//        rxSwift_UITextField()
        rxSwift_UIScrollView()
        
    }
    
    @objc func clickTheButton(sender:UIButton){
        print("传统意义上的按钮点击")
        sender.backgroundColor = UIColor.green
        // 用RxSwift来给textField设置内容
        Observable.just("Hello RxSwift")
            .subscribe(customRxTextField.rx.text)
            .disposed(by: bag)
    }
    
    //RXSwift -- UIView
    func rxSwift_UIView() {
        
        // 用RxSwift来设置customView的背景颜色
        //1、observable负责发出事件,事件上挂的数据就是一个颜色
        //2、customView的rx.backgroundColor属性就是一个binder,所以它可以监听observable,当它收到observable发出的事件时,就会把事件上挂的颜色拿下来真正赋值给customView的backgroundColor属性。还记得我们自己是怎么创建Binder的吧,可以翻回去看一下,RxCocoa底层就是那么实现的
        let observable = Observable.just(UIColor.black)
        let binder = customView.rx.backgroundColor
        observable.bind(to: binder).disposed(by: bag)
        
        // 用RxSwift来设置customView的透明度
        //rx.alpha属性是对传统方式view.setAlpha(...)方法的封装,我们可以用它来设置view的透明度。
        Observable.just(0.618).bind(to: customView.rx.alpha).disposed(by: bag)
        
        // 用RxSwift来设置customView是否隐藏
        //rx.isHidden属性是对传统方式view.setHidden(...)方法的封装,我们可以用它来设置view是否隐藏。
        Observable.just(false).bind(to: customView.rx.isHidden).disposed(by: bag)
        
        
        // 用RxSwift来设置customView是否能够处理用户交互
        //rx.isUserInteractionEnabled属性是对传统方式view.setUserInteractionEnabled(...)方法的封装,我们可以用它来设置view是否能够处理用户交互。
        Observable.just(true).bind(to: customView.rx.isUserInteractionEnabled).disposed(by: bag)
        
    }
    
    //RXSwift -- UILabel
    func rxSwift_UILabel() {
        
        // 用RxSwift来设置label的文本
        //rx.text属性是对传统方式label.setText(...)方法的封装,我们可以用它来设置label的文本。
        Observable.just("Hello RxSwift").bind(to: customLabel.rx.text).disposed(by: bag)
        
        // 用RxSwift来设置label的属性文本
        //rx.attributedText属性是对传统方式label.setAttributedText(...)方法的封装,我们可以用它来设置label的属性文本。
        Observable.just("Hello RxSwift").map({ element in
            print(element)
            let attributedString = NSAttributedString(string: element, attributes: [
                NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
                NSAttributedString.Key.underlineColor: UIColor.red,
            ])
            return attributedString
            }).bind(to: customLabel.rx.attributedText).disposed(by: bag)
        
    }
    
    //RXSwift -- UIImageView
    func rxSwift_UIImageView() {
        
        // 用RxSwift来设置imageView的图片
        //rx.image属性是对传统方式imageView.setImage(...)方法的封装,我们可以用它来设置imageView的图片。
        Observable.just(UIImage.init(named: "idle_0")).bind(to: customImgView.rx.image).disposed(by: bag)
        
    }
    
    func rxTimerTest() {
        
        //诸如NSTimer(Timer)、CADisplayLink都是需要加入到RunLoop中的,这样的定时器都会收到RunLoop的影响;而GCD的定时器DispatchSource则和RunLoop无关,不会受到滑动事件的影响。那我们就可以大胆的猜测,RxSwift封装的定时器也是对DispatchSource的封装。
        //dueTime:从现在到初始化第一次的触发定时器的时间
        //period:初始化之后每次触发的时间间隔
        //scheduler:调度类型
        let rxTimer = Observable<Int>.timer(.seconds(5), period: .milliseconds(2), scheduler: MainScheduler.instance)
        rxTimer.subscribe(onNext: { (num) in
            print("==\(num)==")
        }).disposed(by: bag)
        
    }

    //RXSwift -- UIButton
    func rxSwift_UIButton() {
        
        //rx.tap属性是对传统方式button.addTarget(..., action: #selector(...), for: .touchUpInside)方法的封装,我们可以用它来给button添加touchUpInside状态下的点击事件。
        
        // 用RxSwift来给button添加touchUpInside状态下的点击事件
        customRxBtn.rx.tap.subscribe { _ in
                print("RX按钮被点击了")
        }.disposed(by: bag)
        
        //rx.isEnabled属性是对传统方式button.setEnabled(...)方法的封装,我们可以用它来设置button是否可以点击。
        // 用RxSwift来设置button是否可以点击
        Observable.just(true).bind(to: customRxBtn.rx.isEnabled).disposed(by: bag)
        
        //rx.controlEvent(...)方法是对传统方式button.addTarget(..., action: ..., for: ...)方法的封装,我们可以用它来给button添加任意状态下的点击事件,上面的rx.tap属性底层就是通过这个方法实现的,只不过锁死了touchUpInside状态。
        // 用RxSwift来给button添加任意状态下的点击事件
        customRxBtn.rx.controlEvent(.touchDown).subscribe { _ in
                print("touchDown")
            }.disposed(by: bag)
        
        // 用RxSwift来给button设置不同状态下的文本
        Observable.just("正常状态下的文本").subscribe(customRxBtn.rx.title(for: .normal))
            .disposed(by: bag)
        Observable.just("高亮状态下的文本").subscribe(customRxBtn.rx.title(for: .highlighted))
            .disposed(by: bag)
        
    }
    
    //RXSwift -- UITextField
    func rxSwift_UITextField() {
        //rx.text属性充当Observable角色时,一般被用来监听textField内容的改变(注意:通过这种方式来监听textField内容的改变时,一打开界面就算textField还没成为第一响应者也会触发一次回调)。
        
        // 用RxSwift来监听textField内容的改变
        customRxTextField.rx.text.subscribe(onNext: { element in
            print("textField的内容改变了:\(String(describing: element))")
            }).disposed(by: bag)
        
        // 用RxSwift来设置textField是否密文输入
        Observable.just(true).subscribe(customRxTextField.rx.isSecureTextEntry).disposed(by: bag)
        
        // 用RxSwift来监听textField的不同状态
       customRxTextField.rx.controlEvent(.editingDidBegin).subscribe { _ in
               // textField开始编辑了、光标开始闪动(textField成为第一响应者)
               print("textFieldDidBeginEditing")
           }.disposed(by: bag)
               
       // 一打开界面不会触发,只有真正改变了内容才会触发
        customRxTextField.rx.controlEvent(.editingChanged).subscribe { [weak self] _ in
               // textField的内容改变了
            print("textField的内容改变了:\(String(describing: self?.customRxTextField.text))")
           }.disposed(by: bag)
               
        customRxTextField.rx.controlEvent(.editingDidEnd).subscribe { _ in
               // textField结束编辑了、光标停止闪动
               print("textFieldDidEndEditing")
           }.disposed(by: bag)
               
        customRxTextField.rx.controlEvent(.editingDidEndOnExit).subscribe { _ in
                       // 点击了键盘上的return键结束编辑,紧接着会触发【textField结束编辑了、光标停止闪动】
                       print("textFieldShouldReturn")
                   }.disposed(by: bag)
        
    }
    
    //RXSwift -- UIScrollView
    func rxSwift_UIScrollView() {
        
        //rx.contentOffset属性充当Observable角色时,一般被用来监听scrollView的滚动(注意:通过这种方式来监听scrollView的滚动时,一打开界面就算不滚动scrollView也会触发一次回调)。
        // 用RxSwift来监听scrollView的滚动
        customRxScrollView.rx.contentOffset
            .subscribe(onNext: { contentOffset in
                print("scrollView滚动中:\(contentOffset)")
            }).disposed(by: bag)
        
        //rx.contentOffset属性充当Observer角色时,是对scrollView.setContentOffset(...)方法的封装,我们可以用它来设置内容scrollView的偏移量。
        Observable.just(CGPointMake(0, 400)).subscribe(customRxScrollView.rx.contentOffset).disposed(by: bag)
        
        
        // 用RxSwift来监听scrollView的滚动
        customRxScrollView.rx.willBeginDragging.subscribe { _ in
            print("即将开始拖拽scrollView")
        }.disposed(by: bag)
        
        // 一打开界面不会触发,只有真正滚动了scrollView才会触发
        customRxScrollView.rx.didScroll
                    .subscribe { _ in
                        print("scrollView滚动中:\(self.customRxScrollView.contentOffset)")
                    }.disposed(by: bag)
        
        customRxScrollView.rx.didEndDragging
                    .subscribe { decelerate in
                        if decelerate.element == false {
                            print("scrollView停止滚动");
                        } else {
                            print("已经停止拖拽scrollView,但是scrollView由于惯性还在减速滚动");
                        }
                    }.disposed(by: bag)
        
        // 但是光靠这个方法来判定scrollView停止滚动是有一个bug的,那就是当我们的手指停止拖拽scrollView时、按住屏幕不放手、导致scrollView不滚动,是不会触发这个方法的,而是会触发scrollViewDidEndDragging:willDecelerate:方法,所以严谨来判断应该靠它俩联合
        customRxScrollView.rx.didEndDecelerating.subscribe { _ in
                        print("scrollView停止滚动")
                    }.disposed(by: bag)
        
    }
    
    

    
    
}

三、RXSwift 的subject

在使用RXSwift进行响应式编程时,我们经常会遇到需要在不同的线程中发送和订阅事件的场景。RXSwift提供了一种称为Subject的特殊类型,Subject是RXSwift中一种可以同时充当Observable和Observer的类型。它即可以接受事件,也可以发送事件。

Subject类型

四种不同类型的Subject

  • PublishSubject
  • BehaviorSubject
  • ReplaySubject
  • Variable(已在RXSwift4.0中弃用)
 PublishSubject

PublishSubject它只会向订阅者发送在订阅之后的事件,如果在订阅之前已经发送了事件,那么订阅者将无法接受到这些事件。

        let subject = PublishSubject<String>()
        subject.onNext("Event1")
        
        let subscription1 = subject.subscribe { item in
            print("subscription1 :\(item)")
        }
        
        subject.onNext("Event2")
        
        let subscription2 = subject.subscribe { item in
            print("subscription2 :\(item)")
        }
        
        subject.onNext("Event3")

打印结果:

subscription1 :next(Event2)
subscription1 :next(Event3)
subscription2 :next(Event3)

 subscription1在订阅之后才收到了"Event2"和"Event3"两个事件,而subscription2在订阅后才收到"Event3"这个事件

BehaviorSubject

BehaviorSubject是另一种常用的Subject类型。它会向订阅者发送最新的事件以及之后的事件。当有新的订阅者订阅BehaviorSubject时,它会立即接收到最新的事件。

let subject = BehaviorSubject<String>(value: "InitialEvent")
       
        subject.onNext("Event3")
        
        let subscription1 = subject.subscribe { item in
            print("subscription1 :\(item)")
        }
        
        subject.onNext("Event1")
        
        
        let subscription2 = subject.subscribe { item in
            print("subscription2 :\(item)")
        }
        subject.onNext("Event2")

打印结果:

subscription1 :next(Event3)
subscription1 :next(Event1)
subscription2 :next(Event1)
subscription1 :next(Event2)
subscription2 :next(Event2)

 subscription1在订阅之后先收到上次最新的"Event3"(假如没有subject.onNext("Event3"),会先触发初始化的"InitialEvent"),当subject发送"Event1"事件后, subscription1收到"Event1",subscription2在订阅后先收到上次最新的"Event1",当subject发送"Event2"事件后, subscription1收到"Event2", subscription2也收到"Event2"。

ReplaySubject

ReplaySubject 将对观察者发送全部的元素,无论观察者是何时进行订阅的。

        let disposeBag = DisposeBag()
        
        let subject = ReplaySubject<String>.create(bufferSize: 1)
        
        subject.subscribe { item in
            print("Subscription: 1 Event:\(item)")
        }.disposed(by: disposeBag)
        
        subject.onNext("Event1")
        subject.onNext("Event2")
        
        subject.subscribe { item in
            print("Subscription: 2 Event:\(item)")
        }.disposed(by: disposeBag)
        
        subject.onNext("Event3")
        subject.onNext("Event4")

打印结果:

Subscription: 1 Event:next(Event1)
Subscription: 1 Event:next(Event2)
Subscription: 2 Event:next(Event2)
Subscription: 1 Event:next(Event3)
Subscription: 2 Event:next(Event3)
Subscription: 1 Event:next(Event4)
Subscription: 2 Event:next(Event4)

bufferSize为1,create函数会创建一个ReplayOne对象,Subscription: 2收到了"Event2"事件,如果bufferSize为2,create函数会创建一个Replaymany对象。

RXSwift线程安全

由于Subject是一种共享数据的类型,当多个线程同时访问一个Subject时,可能会导致线程安全问题,RXSwift提供了一个特殊的操作符observeOn,可以用来指定订阅者在哪个线程上接受事件。

        let subject = PublishSubject<String>()
        let queue = DispatchQueue(label: "custom_queue")
        
        subject.observeOn(ConcurrentDispatchQueueScheduler(queue: queue)).subscribe { item in
            print("Event received on: \(item),\(Thread.current)")
        }
        subject.onNext("Event1")

 我们使用observeOn操作符将订阅者的线程切换到自定义的ConcurrentDispatchQueueScheduler队列上。这样可以确保订阅者在指定的队列上接受事件。

四、使用 RxSwift + MVVM 构建ViewModel

方案一:采用Subjects

定义ViewModelType协议:

protocol ViewModelType {
    associatedtype Input
    associatedtype Output

    var input: Input { get }
    var output: Output { get }
}

这样,我们就可以完全自由地选择何时提供输入、何时订阅输出了。
Subject可以同时充当ObserverObservable,把命令式的编程变为Rx的函数式编程。

定义采用Subject的ViewModel:

final class SayHelloViewModel: ViewModelType {
    let input: Input
    let output: Output

    struct Input {
        let name: AnyObserver<String>
        let validate: AnyObserver<Void>
    }

    struct Output {
        let greeting: Driver<String>
    }

    private let nameSubject = ReplaySubject<String>.create(bufferSize: 1)
    private let validateSubject = PublishSubject<Void>()

    init() {
        let greeting = validateSubject
            .withLatestFrom(nameSubject)
            .map { name in
                return "Hello \(name)!"
            }
            .asDriver(onErrorJustReturn: ":-(")

        self.output = Output(greeting: greeting)
        self.input = Input(name: nameSubject.asObserver(), validate: validateSubject.asObserver())
    }
}

 这里有几点值得注意的内容:

1.ViewModel的任务还是输入Input产出Output;

2.Subjectsprivate的,所以你只能通过input和output属性与ViewModel交互;

3.兼具可插拔、可测试的特性,并且充分利用了RxSwift的绑定机制;

View部分的实现:


/// Every view interacting with a `SayHelloViewModel` instance can conform to this.
protocol SayHelloViewModelBindable {
    var disposeBag: DisposeBag? { get }
    func bind(to viewModel: SayHelloViewModel)
}

/// TableViewCells
final class TextFieldCell: UITableViewCell, SayHelloViewModelBindable {
    @IBOutlet weak var nameTextField: UITextField!
    var disposeBag: DisposeBag?

    override func prepareForReuse() {
        super.prepareForReuse()

        // Clean Rx subscriptions
        disposeBag = nil
    }

    func bind(to viewModel: SayHelloViewModel) {
        let bag = DisposeBag()
        nameTextField.rx
            .text
            .orEmpty
            .bind(to: viewModel.input.name)
            .disposed(by: bag)
        disposeBag = bag
    }
}

final class ButtonCell: UITableViewCell, SayHelloViewModelBindable {
    @IBOutlet weak var validateButton: UIButton!
    var disposeBag: DisposeBag?

    override func prepareForReuse() {
        super.prepareForReuse()
        disposeBag = nil
    }

    func bind(to viewModel: SayHelloViewModel) {
        let bag = DisposeBag()
        validateButton.rx
            .tap
            .bind(to: viewModel.input.validate)
            .disposed(by: bag)
        disposeBag = bag
    }
}

final class GreetingCell: UITableViewCell, SayHelloViewModelBindable {
    @IBOutlet weak var greetingLabel: UILabel!
    var disposeBag: DisposeBag?

    override func prepareForReuse() {
        super.prepareForReuse()
        disposeBag = nil
    }

    func bind(to viewModel: SayHelloViewModel) {
        let bag = DisposeBag()
        viewModel.output.greeting
            .drive(greetingLabel.rx.text)
            .disposed(by: bag)
        disposeBag = bag
    }
}

/// View
class TableViewController: UIViewController, UITableViewDataSource {
    static let cellIdentifiers = [
        "TextFieldCell",
        "ButtonCell",
        "GreetingCell"
    ]

    @IBOutlet weak var tableView: UITableView!

    private let viewModel = SayHelloViewModel()
    private let bag = DisposeBag()

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return TableViewController.cellIdentifiers.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: TableViewController.cellIdentifiers[indexPath.row])
        (cell as? SayHelloViewModelBindable)?.bind(to: viewModel)
        return cell!
    }
}

方案二:不采用Subjects

定义ViewModelType协议

protocol ViewModelType {
  associatedtype Input
  associatedtype Output

  func transform(input: Input) -> Output
}

这种方案简单易行,只需要一次性提供Input给ViewModel,然后ViewModel即可给出Output。

创建SayHelloViewModel,它需要知道输入的文本以及按钮点击事件,这就是Input。
然后Output是文本内容。

final class SayHelloViewModel: ViewModelType {
  struct Input {
    let name: Observable<String>
    let validate: Observable<Void>
  }

  struct Output {
    let greeting: Driver<String>
  }

  func transform(input: Input) -> Output {
    let greeting = input.validate
      .withLatestFrom(input.name)
      .map { name in
        return "Hello \(name)!"
      }
      .startWith("")
      .asDriver(onErrorJustReturn: ":-(")

    return Output(greeting: greeting)
  }
}

创建SayHelloViewController:

final class SayHelloViewController: UIViewController {
  
  @IBOutlet weak var nameTextField: UITextField!
  @IBOutlet weak var validateButton: UIButton!
  @IBOutlet weak var greetingLabel: UILabel!
  
  private let viewModel = SayHelloViewModel()
  private let bag = DisposeBag()
  
  override func viewDidLoad() {
    super.viewDidLoad()
    bindViewModel()
  }
  
  private func bindViewModel() {
    let inputs = SayHelloViewModel.Input(name: nameTextField.rx.text.orEmpty.asObservable(),
                                         validate: validateButton.rx.tap.asObservable())
    let outputs = viewModel.transform(input: inputs)
    outputs.greeting
      .drive(greetingLabel.rx.text)
      .disposed(by: bag)
  }
}

ViewModel应该是可插拔的,那么我们可以把之前定义的ViewModel用于其他View吗?
现在,如果我们尝试将之前的ViewModel用于带有TableView的View,会发生什么事情?

/// TableViewCells
final class TextFieldCell: UITableViewCell {
  @IBOutlet weak var nameTextField: UITextField!
}

final class ButtonCell: UITableViewCell {
  @IBOutlet weak var validateButton: UIButton!
}

final class GreetingCell: UITableViewCell {
  @IBOutlet weak var greetingLabel: UILabel!
}

/// ViewController
final class SayHelloViewController: UIViewController, UITableViewDataSource {
    static let cellIdentifiers = [
      "TextFieldCell",
      "ButtonCell",
      "GreetingCell"
    ]

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
      return TableViewController.cellIdentifiers.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
      // Classic dequeue work
    }

    private let viewModel = SayHelloViewModel()
    private let bag = DisposeBag()

    override func viewDidLoad() {
      super.viewDidLoad()
      bindViewModel()
    }

    private func bindViewModel() {
      // Let's discuss about this
      let inputs = SayHelloViewModel.Input(name: 😱😱, validate: 😱😱)
    }
}

然而,我们甚至无法为ViewModel提供Input。因为我们不能在创建ViewModel时就获取到UITableView的内容。所以,使用这种方案有一个前提条件:在创建ViewModel的Input时,可以获得全部所需的资源。

这时,你就需要采用第一种方案了!

总结:

需要根据需要来决定采用哪一种方案。

第一种方案兼容性强,但是定义及使用都略显繁琐。
第二种方案简单易行,但是有一定的局限性。

另一种方案
Model层:PersonModel.swift
import UIKit

class PersonModel: NSObject {
    
    /// 姓名
    var name: String?
    
    /// 性别
    ///
    /// 0-未知,1-男,2-女
    var sex: Int?
    
    /// 年龄
    var age: Int?
    
    init(name: String? = nil, sex: Int? = nil, age: Int? = nil) {
        self.name = name
        self.sex = sex
        self.age = age
    }
    
    init(dict:[String:Any]) {
        name = dict["name"] as? String
        sex = (dict["sex"] as? NSNumber)?.intValue
        age = (dict["age"] as? NSNumber)?.intValue
    }

}
 MVVM层:PersonRxViewModel.swift

import UIKit
import RxSwift


/*
 1、ViewModel层:负责请求数据
 2、ViewModel层:负责处理数据
 3、ViewModel层:负责存储数据
 */

class PersonRxViewModel: NSObject {
    
    // 持有一个_personModel,以便处理数据:VM一对一Model地添加属性并处理,搞成计算属性即可
    private var _personModel: PersonModel?
    init(personModel: PersonModel? = nil) {
        _personModel = personModel
    }
    
    //-----------变化1-----------//
    /// 同时新增一个跟原来同名的vm数组,使用RxSwift:
    /// 1、因为外面tableView要显示的数据就是这个数组里的数据,换句话说tableView要监听这个数组,所以这个数组就不能再定义成普通的数据了,而应该定义成一个Observable,里面的事件挂的数据是数组
    /// 2、又因为这个数组里的数据会随着textField输入文本的变化而变化,换句话说这个数组应该监听textField的文本变化,所以这个数组应该定义成一个Observer
    /// 3、所以最终我们得把这个数组定义成一个Subjects
    var publishPersonVMArray = PublishSubject<[PersonViewModel]>()
    
    /// 我们把原来的personVMArray直接降级成一个私有属性,继续搞它原来负责的事情
    private lazy var _personVMArray = [PersonViewModel]()
    //-----------变化1-----------//
    
    /// 姓名
    var name: String {
        return _personModel?.name ?? ""
    }
        
    /// 性别
    ///
    /// 0-未知,1-男,2-女
    var sex: String {
        if _personModel?.sex == 1 {
            return "男"
        } else if _personModel?.sex == 2 {
            return "女"
        } else {
            return "未知"
        }
    }
        
    /// 年龄
    var age: Int {
        return _personModel?.age ?? 0
    }
    
}

// MARK: - 请求数据
extension PersonRxViewModel {
    /// 请求数据
    func loadData(params: String, completionHandler: @escaping (_ isSuccess: Bool) -> Void) {
        guard let path = Bundle.main.path(forResource: params, ofType: ".plist") else {
            completionHandler(false)
            return
        }
        
        guard let array = NSArray(contentsOfFile: path) as? [[String : Any]] else {
            completionHandler(false)
            return
        }
        
        _personVMArray.removeAll()
        for dict in array {
            let personModel = PersonModel(dict: dict)
            let personVM = PersonViewModel(personModel: personModel)
            _personVMArray.append(personVM)
        }
        completionHandler(true)
        
        
        //-----------变化2-----------//
        // 请求成功后,Observable发出一个next事件把数据发出去
        publishPersonVMArray.onNext(_personVMArray)
        //-----------变化2-----------//
    }
    
}
view层:PersonTableViewCell.swift
class PersonTableViewCell: UITableViewCell {

    @IBOutlet weak var sexLabel: UILabel!
    @IBOutlet weak var ageName: UILabel!
    @IBOutlet weak var nameLabel: UILabel!
    
    override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }
    
    var personVM: PersonViewModel? {
        didSet {
            guard let personVM = personVM else { return }
            
            nameLabel.text = personVM.name
            sexLabel.text = personVM.sex
            ageName.text = "\(personVM.age)"
        }
    }
    
}
 Controller层:PersonRxMVVMController.swift
import UIKit
import SnapKit
import RxSwift
import RxCocoa

class PersonRxMVVMController:UIViewController {
    
    let bag = DisposeBag()
    
    private lazy var _textField: UITextField = {
        let textField = UITextField()
        textField.backgroundColor = UIColor.red
        textField.returnKeyType = .done
        return textField
    }()
    
    lazy var tableView: UITableView = {
        let tableView = UITableView.init(frame: .zero, style: .grouped)
        tableView.backgroundColor = .white
        tableView.estimatedSectionHeaderHeight = 0
        tableView.estimatedSectionFooterHeight = 0
        tableView.register(UINib(nibName: "PersonTableViewCell", bundle: nil), forCellReuseIdentifier: "cellId")
        return tableView
    }()
    
    private lazy var _personVM = PersonRxViewModel()
    
    private var _insertText = "张";

    override func viewDidLoad() {
        super.viewDidLoad()
        
        _addViews()
        _layoutViews()
        
        // 再把数据和UI进行双向绑定
        _setupRxSwift()
        
        // 初始请求数据
        _loadData(params: "person")
        
    }

}

extension PersonRxMVVMController: UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 100
    }
}


// MARK: - setupRxSwift
extension PersonRxMVVMController {
    private func _setupRxSwift() {
        // 数据绑定到tableView上:数据驱动UI
        _personVM.publishPersonVMArray.bind(to: tableView.rx.items) {
                tableView, indexPathRow, data in
                let cell = tableView.dequeueReusableCell(withIdentifier: "cellId", for: IndexPath(row: indexPathRow, section: 0))  as! PersonTableViewCell
                cell.personVM = data
                return cell
            }
            .disposed(by: bag)
        
        tableView.rx.setDelegate(self).disposed(by: bag)
        
        
        // textField的内容发生变化后,重新请求数据:UI驱动数据
       _textField.rx.controlEvent(.editingDidEnd).subscribe { _ in
               // textField结束编辑了、光标停止闪动
            print(self._textField.text ?? "")
//               self._insertText = self._textField.text ?? ""
//               self._personVM.loadData(params: self._insertText) { isSuccess in
//                   if !isSuccess {
//                       print("请求数据出错")
//                   }
//               }
           }.disposed(by: bag)
        
        _textField.rx.controlEvent(.editingDidEndOnExit).subscribe { _ in
                // 点击了键盘上的return键结束编辑,紧接着会触发【textField结束编辑了、光标停止闪动】
                print("textFieldShouldReturn")
            }.disposed(by: bag)
    }
    
}


// MARK: - 请求数据
extension PersonRxMVVMController {
    private func _loadData(params: String) {
        _personVM.loadData(params: params) { isSuccess in
            if !isSuccess {
                print("请求数据出错")
            }
        }
    }
}

// MARK: - setupUI
extension PersonRxMVVMController {
    private func _addViews() {
        view.addSubview(_textField)
        view.addSubview(tableView)
    }
    
    private func _layoutViews() {
        _textField.frame = CGRect(x: 0, y: 20, width: view.bounds.size.width, height: 44)
        tableView.snp.makeConstraints { make in
            make.left.equalTo(view)
            make.right.equalTo(view)
            make.top.equalTo(_textField.bottom).offset(0)
            make.bottom.equalTo(view).offset(-SafeBottomHeight-44)
        }
        //tableView.frame = CGRect(x: 0, y: 64, width: view.bounds.size.width, height: view.bounds.size.height - 64)
    }
}

Jetpack是一个结合MVVM的快速开发框架,它基于MVVM模式并集成了谷歌官方推荐的Jetpack组件库,包括LiveData、ViewModel、Lifecycle和Navigation组件。这个框架使用Kotlin语言,并添加了大量的拓展函数,以简化代码。它还集成了Retrofit网络请求和协程,可以帮助开发者快速开发项目。\[1\] MVVM是一种软件架构模式,它将应用程序分为三个主要部分:模型(Model)、视图(View)和视图模型(ViewModel)。在MVVM中,视图负责显示数据和用户交互,模型负责处理数据和业务逻辑,而视图模型则充当视图和模型之间的中间层,负责管理视图的状态和数据。Jetpack的MVVM模式可以帮助开发者更好地组织和管理代码,提高开发效率和舒适度。\[1\] 使用Jetpack和MVVM可以带来许多好处,例如简化代码、提高开发效率、提供更好的代码结构和可维护性。通过使用Jetpack的组件库,开发者可以更轻松地处理生命周期管理、数据共享和导航等常见任务。而MVVM模式则可以帮助开发者更好地分离关注点,使代码更易于测试和维护。\[2\] 总之,Jetpack和MVVM是一种强大的组合,可以帮助开发者快速开发Android应用程序,并提供更好的代码结构和可维护性。如果你想了解更多关于Jetpack和MVVM的信息,可以参考引用\[1\]中提供的Jetpack框架的介绍。 #### 引用[.reference_title] - *1* *2* [JetpackMvvm](https://blog.csdn.net/u014608640/article/details/124711159)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [大型Android项目架构:基于组件化+模块化+Kotlin+协程+Flow+Retrofit+Jetpack+MVVM架构实现WanAndroid...](https://blog.csdn.net/m0_37796683/article/details/130277908)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insertT0,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值