【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大全

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章