【RxSwift】RxSwift在MVVM方面的实际应用

目录
一、首先我们看看RxCocoa做了啥
  1、UIView
  2、UILabel
  3、UIImageView
  4、UIButton
  5、UITextField
  6、UIScrollView
  7、UITableView
  8、UICollectionView
二、然后我们写个MVVM的小案例
  1、不使用RxSwift时的MVVM
  2、使用RxSwift时的MVVM


一、首先我们看看RxCocoa做了啥


为了帮助我们更加简单优雅地实现ViewModel和View的双向绑定,RxCocoa已经帮我们把UIKit框架里常用控件的常用属性都搞成了Observable或Binder、有的属性甚至是Subjects,这样有的属性就可以发出事件(以便让数据监听),有的属性就可以监听Observable——即数据,有的属性既可以发出事件、也可以监听Observable,因此在实际开发中UI这边儿直接拿现成的用就行了,通常情况下我们只需要把数据定义成Subjects——这样数据就可以发出事件(以便让UI监听)、也可以监听Observable——即UI。

1、UIView

UIView的rx.backgroundColorrx.alpharx.isHiddenrx.isUserInteractionEnabled属性都是Binder,所以它们可以监听Observable。

  • rx.backgroundColor属性

rx.backgroundColor属性是对传统方式view.setBackgroundColor(...)方法的封装,我们可以用它来设置view的背景颜色。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var customView: UIView!
    
    let bag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 用RxSwift来设置customView的背景颜色
        //
        // 1、observable负责发出事件,事件上挂的数据就是一个颜色
        // 2、customView的rx.backgroundColor属性就是一个binder,所以它可以监听observable,当它收到observable发出的事件时,就会把事件上挂的颜色拿下来真正赋值给customView的backgroundColor属性。还记得我们自己是怎么创建Binder的吧,可以翻回去看一下,RxCocoa底层就是那么实现的
        let observable = Observable.just(UIColor.red)
        let binder = customView.rx.backgroundColor
        observable.bind(to: binder).disposed(by: bag)
    }
}
  • rx.alpha属性

rx.alpha属性是对传统方式view.setAlpha(...)方法的封装,我们可以用它来设置view的透明度。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var customView: UIView!

    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 用RxSwift来设置customView的透明度
        Observable.just(0.618)
            .bind(to: customView.rx.alpha)
            .disposed(by: bag)
    }
}
  • rx.isHidden属性

rx.isHidden属性是对传统方式view.setHidden(...)方法的封装,我们可以用它来设置view是否隐藏。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var customView: UIView!

    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 用RxSwift来设置customView是否隐藏
        Observable.just(true)
            .bind(to: customView.rx.isHidden)
            .disposed(by: bag)
    }
}
  • rx.isUserInteractionEnabled属性

rx.isUserInteractionEnabled属性是对传统方式view.setUserInteractionEnabled(...)方法的封装,我们可以用它来设置view是否能够处理用户交互。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var customView: UIView!

    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 用RxSwift来设置customView是否能够处理用户交互
        Observable.just(false)
            .bind(to: customView.rx.isUserInteractionEnabled)
            .disposed(by: bag)
    }
}

2、UILabel

UILabel的rx.textrx.attributedText属性都是Binder,所以它们可以监听Observable。

  • rx.text属性

rx.text属性是对传统方式label.setText(...)方法的封装,我们可以用它来设置label的文本。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var label: UILabel!

    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 用RxSwift来设置label的文本
        Observable.just("Hello RxSwift")
            .bind(to: label.rx.text)
            .disposed(by: bag)
    }
}
  • rx.attributedText属性

rx.attributedText属性是对传统方式label.setAttributedText(...)方法的封装,我们可以用它来设置label的属性文本。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var label: UILabel!

    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 用RxSwift来设置label的属性文本
        Observable.just("Hello RxSwift")
            .map({ element in
                let attributedString = NSAttributedString(string: element, attributes: [
                    NSAttributedString.Key.underlineStyle: NSUnderlineStyle.single.rawValue,
                    NSAttributedString.Key.underlineColor: UIColor.red,
                ])
                return attributedString
            })
            .bind(to: label.rx.attributedText)
            .disposed(by: bag)
    }
}

3、UIImageView

UIImageView的rx.image属性是Binder,所以它可以监听Observable。

  • rx.image属性

rx.image属性是对传统方式imageView.setImage(...)方法的封装,我们可以用它来设置imageView的图片。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var imageView: UIImageView!

    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 用RxSwift来设置imageView的图片
        Observable<Int>.timer(.seconds(0), period: .milliseconds(167), scheduler: MainScheduler.instance)
            .map({ element in
                let imageName = "idle_\(element)" // 映射出图片名称
                let image = UIImage(named: imageName)!
                return image
            })
            .bind(to: imageView.rx.image)
            .disposed(by: bag)
    }
}

4、UIButton

UIButton的rx.tap属性是Observable,所以它可以发出事件。

UIButton的rx.isEnabledrx.isSelected属性都是Observer,所以它们可以监听Observable。

UIButton的rx.controlEvent(...)方法的返回值是Observable,所以它可以发出事件。

UIButton的rx.title(for: ...)rx.image(for: ...)rx.backgroundImage(for: ...)方法的返回值都是Binder,所以它们的返回值可以监听Observable。

  • rx.tap属性

rx.tap属性是对传统方式button.addTarget(..., action: #selector(...), for: .touchUpInside)方法的封装,我们可以用它来给button添加touchUpInside状态下的点击事件。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var button: UIButton!

    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 用RxSwift来给button添加touchUpInside状态下的点击事件
        button.rx.tap
            .subscribe { _ in
                print("按钮被点击了")
            }
            .disposed(by: bag)
    }
}
  • rx.isEnabled属性

rx.isEnabled属性是对传统方式button.setEnabled(...)方法的封装,我们可以用它来设置button是否可以点击。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var button: UIButton!

    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 用RxSwift来设置button是否可以点击
        Observable<Int>.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance)
            .map({ element in
                let value = element % 2 == 0 // 映射为bool值
                return value
            })
            .bind(to: button.rx.isEnabled)
            .disposed(by: bag)
    }
}
  • rx.isSelected属性

rx.isSelected属性是对传统方式button.setSelected(...)方法的封装,我们可以用它来设置button是否处于选中状态。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var button: UIButton!

    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 用RxSwift来设置button是否可以点击
        Observable<Int>.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance)
            .map({ element in
                let value = element % 2 == 0 // 映射为bool值
                return value
            })
            .bind(to: button.rx.isSelected)
            .disposed(by: bag)
    }
}
  • rx.controlEvent(...)方法

rx.controlEvent(...)方法是对传统方式button.addTarget(..., action: ..., for: ...)方法的封装,我们可以用它来给button添加任意状态下的点击事件,上面的rx.tap属性底层就是通过这个方法实现的,只不过锁死了touchUpInside状态。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var button: UIButton!

    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 用RxSwift来给button添加任意状态下的点击事件
        button.rx.controlEvent(.touchDown)
            .subscribe { _ in
                print("touchDown")
            }
            .disposed(by: bag)
    }
}
  • rx.title(for: ...)方法

rx.title(for: ...)方法是对传统方式button.setTitle(..., for: ...)方法的封装,我们可以用它来给button设置不同状态下的文本。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var button: UIButton!

    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 用RxSwift来给button设置不同状态下的文本
        Observable.just("正常状态下的文本")
            .subscribe(button.rx.title(for: .normal))
            .disposed(by: bag)
        Observable.just("高亮状态下的文本")
            .subscribe(button.rx.title(for: .highlighted))
            .disposed(by: bag)
    }
}
  • rx.image(for: ...)方法

rx.image(for: ...)方法是对传统方式button.setImage(..., for: ...)方法的封装,我们可以用它来给button设置不同状态下的图片。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var button: UIButton!

    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 用RxSwift来给button设置不同状态下的图片
        Observable.just(UIImage(named: "idle_0"))
            .subscribe(button.rx.image(for: .normal))
            .disposed(by: bag)
        Observable.just(UIImage(named: "idle_59"))
            .subscribe(button.rx.image(for: .highlighted))
            .disposed(by: bag)
    }
}
  • rx.backgroundImage(for: ...)方法

rx.backgroundImage(for: ...)方法是对传统方式button.setBackgroundImage(..., for: ...)方法的封装,我们可以用它来给button设置不同状态下的背景图片。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var button: UIButton!

    let bag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        // 用RxSwift来给button设置不同状态下的图片
        Observable.just(UIImage(named: "idle_1"))
            .subscribe(button.rx.backgroundImage(for: .normal))
            .disposed(by: bag)
        Observable.just(UIImage(named: "idle_58"))
            .subscribe(button.rx.backgroundImage(for: .highlighted))
            .disposed(by: bag)
    }
}

5、UITextField

UITextField的rx.text属性是Subjects,所以它既可以发出事件、也可以监听Observable。

UITextField的rx.isSecureTextEntry是Observer,所以它可以监听Observable。

UITextField的rx.controlEvent(...)方法的返回值是Observable,所以它可以发出事件。

  • rx.text属性

rx.text属性充当Observable角色时,一般被用来监听textField内容的改变(注意:通过这种方式来监听textField内容的改变时,一打开界面就算textField还没成为第一响应者也会触发一次回调)。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    
    let bag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 用RxSwift来监听textField内容的改变
        textField.rx.text
            .subscribe(onNext: { element in
                print("textField的内容改变了:\(element)")
            })
            .disposed(by: bag)
    }
}

rx.text属性充当Observer角色时,是对textField.setText(...)方法的封装,我们可以用它来给textField设置内容。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    
    let bag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 用RxSwift来给textField设置内容
        Observable.just("Hello RxSwift")
            .subscribe(textField.rx.text)
            .disposed(by: bag)
    }
}
  • rx.isSecureTextEntry属性

rx.isSecureTextEntry是对textField.setSecureTextEntry(...)方法的封装,我们可以用它来设置textField是否密文输入。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    
    let bag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 用RxSwift来设置textField是否密文输入
        Observable.just(true)
            .subscribe(textField.rx.isSecureTextEntry)
            .disposed(by: bag)
    }
}
  • rx.controlEvent(...)方法

rx.controlEvent(...)方法是对传统方式textField一堆代理方法的封装,我们可以用它来监听textField的不同状态,上面的rx.text属性底层就是通过这个方法实现的,只不过锁死了editingChanged状态。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var textField: UITextField!
    
    let bag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 用RxSwift来监听textField的不同状态
        textField.rx.controlEvent(.editingDidBegin)
            .subscribe { _ in
                // textField开始编辑了、光标开始闪动(textField成为第一响应者)
                print("textFieldDidBeginEditing")
            }
            .disposed(by: bag)
        
        // 一打开界面不会触发,只有真正改变了内容才会触发
        textField.rx.controlEvent(.editingChanged)
            .subscribe { [weak self] _ in
                // textField的内容改变了
                print("textField的内容改变了:\(self.textField.text)")
            }
            .disposed(by: bag)
        
        textField.rx.controlEvent(.editingDidEnd)
            .subscribe { _ in
                // textField结束编辑了、光标停止闪动
                print("textFieldDidEndEditing")
            }
            .disposed(by: bag)
        
        textField.rx.controlEvent(.editingDidEndOnExit)
            .subscribe { _ in
                // 点击了键盘上的return键结束编辑,紧接着会触发【textField结束编辑了、光标停止闪动】
                print("textFieldShouldReturn")
            }
            .disposed(by: bag)
    }
}

6、UIScrollView

UIScrollView的rx.contentOffset属性是Subjects,所以它既可以发出事件、也可以监听Observable。

UIScrollView的rx.willBeginDraggingrx.didScrollrx.didEndDraggingrx.didEndDecelerating属性都是Observable,所以它们可以发出事件。

  • rx.contentOffset属性

rx.contentOffset属性充当Observable角色时,一般被用来监听scrollView的滚动(注意:通过这种方式来监听scrollView的滚动时,一打开界面就算不滚动scrollView也会触发一次回调)。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var scrollView: UIScrollView!
    
    let bag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.contentSize = CGSize(width: 0, height: 1000)
        
        // 用RxSwift来监听scrollView的滚动
        scrollView.rx.contentOffset
            .subscribe(onNext: { contentOffset in
                print("scrollView滚动中:\(contentOffset)")
            })
            .disposed(by: bag)
    }
}

rx.contentOffset属性充当Observer角色时,是对scrollView.setContentOffset(...)方法的封装,我们可以用它来设置内容scrollView的偏移量。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var scrollView: UIScrollView!
    
    let bag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.contentSize = CGSize(width: 0, height: 1000)
        
        // 用RxSwift来设置内容scrollView的偏移量
        Observable<Int>.timer(.seconds(0), period: .seconds(1), scheduler: MainScheduler.instance)
            .map({ element in
                let point = CGPoint(x: 0, y: 10 * element) // 映射出点
                return point
            })
            .bind(to: scrollView.rx.contentOffset)
            .disposed(by: bag)
    }
}
  • rx.willBeginDraggingrx.didScrollrx.didEndDraggingrx.didEndDecelerating属性

这一堆属性是对传统方式scrollView一堆代理方法的封装,我们可以用它们来监听scrollView的滚动。

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    @IBOutlet weak var scrollView: UIScrollView!
    
    let bag = DisposeBag()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        scrollView.contentSize = CGSize(width: 0, height: 1000)
        
        // 用RxSwift来监听scrollView的滚动
        scrollView.rx.willBeginDragging
            .subscribe { _ in
                print("即将开始拖拽scrollView")
            }
            .disposed(by: bag)
        
        // 一打开界面不会触发,只有真正滚动了scrollView才会触发
        scrollView.rx.didScroll
            .subscribe { _ in
                print("scrollView滚动中:\(self.scrollView.contentOffset)")
            }
            .disposed(by: bag)
        
        scrollView.rx.didEndDragging
            .subscribe { decelerate in
                if decelerate.element == false {
                    print("scrollView停止滚动");
                } else {
                    print("已经停止拖拽scrollView,但是scrollView由于惯性还在减速滚动");
                }
            }
            .disposed(by: bag)
        
        // 但是光靠这个方法来判定scrollView停止滚动是有一个bug的,那就是当我们的手指停止拖拽scrollView时、按住屏幕不放手、导致scrollView不滚动,是不会触发这个方法的,而是会触发scrollViewDidEndDragging:willDecelerate:方法,所以严谨来判断应该靠它俩联合
        scrollView.rx.didEndDecelerating
            .subscribe { _ in
                print("scrollView停止滚动")
            }
            .disposed(by: bag)
    }
}

7、UITableView

UITableView这块儿,我们就不像上面那样一个一个属性或方法详细说了,直接演示下怎么用,因为这块儿的内容实在太多了,可以自己点进去UITableView+Rx.swift文件去看去分析。

UITableView这块儿,Observable通常有两种,一是tableView要显示的数据——也就是说我们得把tableView要显示的数据给手动搞成一个Observable,然后让tableView的一堆东西——即Observer来监听这个Observable就可以了,这样数据发生变化时,tableView的显示就会跟着自动发生变化,非常符合数据驱动UI的理念;二是用户对tableView做的操作——如点击cell、插入cell、删除cell、移动cell等;其它的都是Observer。

  • 实现单个分区的UITableView

1️⃣我们不需要像传统方式那样实现numberOfSectionsInTableView:代理方法告诉tableView有一个分区
2️⃣我们也不需要像传统方式那样调用numberOfRowsInSection:告诉tableView分区里有多少个cell
3️⃣我们只需要实现cellForRowAtIndexPath:这一个代理方法(是一个Observer),让它来监听数据(是一个Observable)就可以了,RxSwift会自动给我们搞好有一个分区、分区里有多少个cell这些事

import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    let bag = DisposeBag()
    
    // tableView
    lazy var tableView: UITableView = {
        let tableView = UITableView(frame: CGRect.zero, style: .plain)
        tableView.backgroundColor = UIColor.clear
        return tableView
    }()
    
    // tableView要显示的数据
    //
    // 不要定义成普通数组,要定义成Observable,让它发出的事件里挂数组,这样才能让tableView监听,达到数据驱动UI的效果
    var dataArray = Observable.just([
        "张一",
        "张二",
        "张三",
    ])

    override func viewDidLoad() {
        super.viewDidLoad()

        _addViews()
        _layoutViews()
        _setupRxSwift()
    }
}


// MARK: - setupRxSwift

extension ViewController {
    private func _setupRxSwift() {
        // 让tableView.rx.items属性监听数据就可以了,就这么简单,搞定
        //
        // dataArray是个Observable
        // tableView.rx.items是个Observer(本质就是对cellForRowAtIndexPath:代理方法的封装)
        dataArray
            .bind(to: tableView.rx.items) {
                tableView, indexPathRow, data in
                var cell = tableView.dequeueReusableCell(withIdentifier: "reuseId")
                if cell == nil {
                    cell = UITableViewCell(style: .default, reuseIdentifier: "reuseId")
                }
                
                cell?.textLabel?.text = data
                
                return cell!
            }
            .disposed(by: bag)
    }
}


// MARK: - addViews, layoutViews

extension ViewController {
    private func _addViews() {
        view.addSubview(tableView)
    }
    
    private func _layoutViews() {
        tableView.frame = view.bounds
    }
}

4️⃣那怎么监听cell的点击呢?上面我们说过“用户对tableView做的操作都是Observable”,所以很简单,拿个闭包监听这个Observable就行了

// MARK: - setupRxSwift

extension ViewController {
    private func _setupRxSwift() {
        // 监听cell的点击,获取那一行对应的row
        tableView.rx.itemSelected
            .subscribe(onNext: { indexPath in
                print("点击了cell,对应的row为:\(indexPath.row)")
            })
            .disposed(by: bag)
        
        // 监听cell的点击,获取那一行对应的data
        tableView.rx.modelSelected(String.self)
            .subscribe(onNext: { data in
                print("点击了cell,对应的data为:\(data)")
            })
            .disposed(by: bag)
    }
}

5️⃣要想设置cell的高度、section header和section header的高度、section footer和section footer的高度,得通过Rx提供的方法设置代理并实现相关的代理方法

// MARK: - setupRxSwift

extension ViewController {
    private func _setupRxSwift() {
        tableView.rx.setDelegate(self).disposed(by: bag)
    }
}


// MARK: - UITableViewDelegate

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

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let label = UILabel()
        label.text = "我是区头"
        return label
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 44
    }

    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        let label = UILabel()
        label.text = "我是区尾"
        return label
    }
    
    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        return 44
    }
}

6️⃣至于用户能对tableView做的其它编辑操作——如插入cell、删除cell、移动cell等,你只要知道它们肯定都是Observable就行了,找相应的API实现即可。

  • 实现多个分区的UITableView

要想实现多个分区的UITableView,必须得安装RxDataSources这个框架。

pod 'RxSwift', '~> 5.0'
pod 'RxCocoa', '~> 5.0'
pod 'RxDataSources', '~> 5.0'

然后在想使用的地方导入这个框架。

// 这个框架的本质就是使用RxSwift对UITableView和UICollectionView的数据源做了一层包装,使用它可以大大减少我们的工作量
import RxDataSources

1️⃣同样地,我们只需要实现cellForRowAtIndexPath:这一个代理方法(是一个Observer),让它来监听数据(是一个Observable)就可以了,RxSwift会自动给我们搞好有多少个分区、每个分区里有多少个cell这些事,不过需要注意的是:RxDataSources是专门用来做多分区的,所以在给它传数据时,dataArray里就不能是普通的数据,而必须得是SectionModel或其子类的数据,这也很好理解,一个section对应一个sectionModel嘛,跟单分区的UITableView就这个地方有区别:构建数据 + 数据绑定到tableView。

import UIKit
import RxSwift
import RxCocoa
import RxDataSources

class ViewController: UIViewController {
    let bag = DisposeBag()
    
    // tableView
    lazy var tableView: UITableView = {
        let tableView = UITableView(frame: CGRect.zero, style: .plain)
        tableView.backgroundColor = UIColor.clear
        return tableView
    }()
    
    // tableView要显示的数据
    //
    // 不要定义成普通数组,要定义成Observable,让它发出的事件里挂数组,这样才能让tableView监听,达到数据驱动UI的效果
    var dataArray = Observable.just([
        // 数据这儿有几个CustomSectionModel,tableView就会有几个分区
        CustomSectionModel(identityText: "", items: [
            // 每个分区里的数据
            "张一",
            "张二",
            "张三",
        ]),
        CustomSectionModel(identityText: "", items: [
            "李一",
            "李二",
            "李三",
            "李四",
        ]),
        CustomSectionModel(identityText: "", items: [
            "王一",
            "王二",
            "王三",
            "王四",
            "王五",
        ]),
    ])

    override func viewDidLoad() {
        super.viewDidLoad()
                
        _addViews()
        _layoutViews()
        _setupRxSwift()
    }
}


// MARK: - setupRxSwift

extension ViewController {
    private func _setupRxSwift() {
        // 让tableView.rx.items(...)方法的返回值监听数据就可以了,就这么简单,搞定
        //
        // dataArray是个Observable
        // tableView.rx.items(...)方法的返回值是个Observer
        dataArray
            .bind(to: tableView.rx.items(dataSource: RxTableViewSectionedReloadDataSource<CustomSectionModel>( configureCell: {
                (dataSource, tableView, indexPath, data) -> UITableViewCell in
                var cell = tableView.dequeueReusableCell(withIdentifier: "reuseId")
                if cell == nil {
                    cell = UITableViewCell(style: .default, reuseIdentifier: "reuseId")
                }
                
                cell?.textLabel?.text = data
                
                return cell!
            })))
            .disposed(by: bag)
        
        
        
        tableView.rx.setDelegate(self).disposed(by: bag)
    }
}


// MARK: - UITableViewDelegate

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

    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        let label = UILabel()
        label.text = "我是区头"
        return label
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        return 44
    }
}


// MARK: - addViews, layoutViews

extension ViewController {
    private func _addViews() {
        view.addSubview(tableView)
    }
    
    private func _layoutViews() {
        tableView.frame = view.bounds
    }
}


// MARK: - 自定义SectionModel

struct CustomSectionModel {
    // 该分区的唯一标识,可以定义成任意类型
    // 这里我们定义成String类型了,没什么特殊需求的话传个空字符串""进来就行了
    var identityText: String
    
    // 该分区里的数据
    // 这里我们分区里的数据都是String类型
    var items: [String]
}

extension CustomSectionModel: AnimatableSectionModelType {
    var identity: String {
        return identityText
    }
        
    init(original: CustomSectionModel, items: [String]) {
        self = original
        self.items = items
    }
}

2️⃣其它的实现跟单分区的UITableView一样。

8、UICollectionView

UICollectionView和UITableView差不多。


二、然后我们写个MVVM的小案例


需求很简单:

  • 用一个tableView显示用户的姓名、性别、年龄
  • textField输入“张”时就请求张姓的用户显示,textField输入“李”时就请求李姓的用户显示

1、不使用RxSwift时的MVVM

  • Model层:PersonModel.swift
/*
 Model层的数据搞成最原始的数据即可
 */
import UIKit

class PersonModel: NSObject {
    /// 姓名
    var name: String?
    
    /// 性别
    ///
    /// 0-未知,1-男,2-女
    var sex: Int?
    
    /// 年龄
    var age: Int?
    
    init(dict: [String : Any]) {
        name = dict["name"] as? String
        sex = (dict["sex"] as? NSNumber)?.intValue
        age = (dict["age"] as? NSNumber)?.intValue
    }
}
  • View层:TableViewCell.swift
/*
 View层持有vm,直接拿着数据展示即可
 */
import UIKit

class TableViewCell: UITableViewCell {
    @IBOutlet weak var nameLabel: UILabel!
    @IBOutlet weak var sexLabel: UILabel!
    @IBOutlet weak var ageLabel: UILabel!
    
    var personVM: PersonViewModel? {
        didSet {
            guard let personVM = personVM else { return }
            
            nameLabel.text = personVM.name
            sexLabel.text = personVM.sex
            ageLabel.text = "\(personVM.age)"
        }
    }
}
  • ViewModel层:PersonViewModel.swift
/*
 1、ViewModel层:负责请求数据
 2、ViewModel层:负责处理数据
 3、ViewModel层:负责存储数据
 */
import UIKit

class PersonViewModel {
    // 持有一个_personModel,以便处理数据:VM一对一Model地添加属性并处理,搞成计算属性即可
    private var _personModel: PersonModel?
    init(personModel: PersonModel? = nil) {
        _personModel = personModel
    }
    
    /// vm数组
    ///
    /// 真正暴露给外面使用的是vm数组,里面的数据已经处理好了,直接拿着显示就行了
    lazy var personVMArray = [PersonViewModel]()
    
    /// 姓名
    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 PersonViewModel {
    /// 请求数据
    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
        }
        
        for dict in array {
            let personModel = PersonModel(dict: dict)
            let personVM = PersonViewModel(personModel: personModel)
            personVMArray.append(personVM)
            completionHandler(true)
        }
    }
}
  • Controller层:ViewController.swift
/*
 1、Controller层:持有view,创建并把view添加到界面上
 2、Controller层:持有vm,调用vm的方法请求数据
 3、vm --> view:Controller调用vm的方法请求数据,请求完成后vm是通过block的方式告诉Controller的,Controller可以调用一下view的reloadData方法把vm里最新存储的数据赋值给view显示
 4、view --> vm:view产生的变化是通过代理告诉Controller的,Controller可以调用vm的方法把view发生的变化告诉它
 */
import UIKit

class ViewController: UIViewController {
    private lazy var _textField: UITextField = {
        let textField = UITextField()
        textField.backgroundColor = UIColor.red
        textField.returnKeyType = .done
        textField.delegate = self
        return textField
    }()
    
    private lazy var _tableView: UITableView = {
        let tableView = UITableView()
        tableView.backgroundColor = UIColor.green
        tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "cell")
        tableView.dataSource = self
        tableView.delegate = self
        return tableView
    }()
    
    private lazy var _personVM = PersonViewModel()
    
    private var _insertText = "张";
    
    override func viewDidLoad() {
        super.viewDidLoad()

        _addViews()
        _layoutViews()
        _loadData(params: _insertText)
    }
}


// MARK: - UITextFieldDelegate

extension ViewController: UITextFieldDelegate {
    func textFieldDidEndEditing(_ textField: UITextField) {
        _insertText = textField.text ?? ""
        _personVM.loadData(params: _insertText) { isSuccess in
            if isSuccess {
                self._tableView.reloadData()
            } else {
                print("请求数据出错")
            }
        }
    }
    
    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
        view.endEditing(true)
        return true
    }
}


// MARK: - UITableViewDataSource, UITableViewDelegate

extension ViewController: UITableViewDataSource, UITableViewDelegate {
    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return _personVM.personVMArray.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath) as! TableViewCell
        cell.personVM = _personVM.personVMArray[indexPath.row];
        return cell
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return 100
    }
}


// MARK: - 请求数据

extension ViewController {
    private func _loadData(params: String) {
        _personVM.loadData(params: params) { isSuccess in
            if isSuccess {
                self._tableView.reloadData()
            } else {
                print("请求数据出错")
            }
        }
    }
}


// MARK: - setupUI

extension ViewController {
    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.frame = CGRect(x: 0, y: 64, width: view.bounds.size.width, height: view.bounds.size.height - 64)
    }
}

2、使用RxSwift时的MVVM

  • Model层和View层都不需要改动

  • ViewModel层:PersonViewModel.swift

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

class PersonViewModel {
    // 持有一个_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 personVMArray = 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 PersonViewModel {
    /// 请求数据
    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事件把数据发出去
        personVMArray.onNext(_personVMArray)
        //-----------变化2-----------//
    }
}
  • Controller层:ViewController.swift
/*
 1、Controller层:持有view,创建并把view添加到界面上
 2、Controller层:持有vm,调用vm的方法请求数据
 3、vm --> view:Controller调用vm的方法请求数据,请求完成后vm是通过block的方式告诉Controller的,但是Controller只需要处理出错的情况,成功的情况什么都不需要做,因为数据已经自动驱动UI了——数据已经绑定到tableView上了
 4、view --> vm:view产生的变化不再是通过代理而是通过Rx的链式调用告诉Controller的,Controller可以调用vm的方法把view发生的变化告诉它
 */
import UIKit
import RxSwift
import RxCocoa

class ViewController: UIViewController {
    let bag = DisposeBag()
    
    private lazy var _textField: UITextField = {
        let textField = UITextField()
        textField.backgroundColor = UIColor.red
        textField.returnKeyType = .done
        return textField
    }()
    
    private lazy var _tableView: UITableView = {
        let tableView = UITableView()
        tableView.backgroundColor = UIColor.green
        tableView.register(UINib(nibName: "TableViewCell", bundle: nil), forCellReuseIdentifier: "cell")
        return tableView
    }()
    
    private lazy var _personVM = PersonViewModel()
    
    private var _insertText = "张";
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 数据已经在VM里搞定了,所以这里先搞定UI
        _addViews()
        _layoutViews()
        
        // 再把数据和UI进行双向绑定
        _setupRxSwift()
        
        // 初始请求数据
        _loadData(params: _insertText)
    }
}


// MARK: - setupRxSwift

extension ViewController {
    private func _setupRxSwift() {
        // 数据绑定到tableView上:数据驱动UI
        _personVM.personVMArray
            .bind(to: _tableView.rx.items) {
                tableView, indexPathRow, data in
                let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: IndexPath(row: indexPathRow, section: 0))  as! TableViewCell
                cell.personVM = data
                return cell
            }
            .disposed(by: bag)
        
        _tableView.rx.setDelegate(self).disposed(by: bag)
        
        
        // textField的内容发生变化后,重新请求数据:UI驱动数据
        _textField.rx.controlEvent(.editingDidEnd)
            .subscribe { _ in
                // textField结束编辑了、光标停止闪动
                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)
    }
}

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


// MARK: - 请求数据

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


// MARK: - setupUI

extension ViewController {
    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.frame = CGRect(x: 0, y: 64, width: view.bounds.size.width, height: view.bounds.size.height - 64)
    }
}

参考
1、RxSwift中文文档
2、RxSwift大全

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 156,907评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,546评论 1 289
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,705评论 0 238
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,624评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,940评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,371评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,672评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,396评论 0 195
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,069评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,350评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,876评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,243评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,847评论 3 231
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,004评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,755评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,378评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,266评论 2 259

推荐阅读更多精彩内容