iOS · reactive programming · ReactiveX

Subject và Variable trong RxSwift (Reactive Programming)

Trong các bài trước, mình đã giới thiệu về Stream, Observable, Operator và Scheduler. Trong phần này, mình sẽ giới thiệu về Subject và Variable, các thành phần đóng vai trò như những cầu nối giữa Rx và các mô hình, thư viện không sử dụng Rx.

1. Subject

Trong Reactive Programming, Observable là nơi khởi nguồn, là gốc của  mọi stream.Nhưng có những trường hợp, chúng ta không thể đơn thuần bọc một đối tượng bên trong một Observable, sau đó phát ra những tín hiệu.

Chẳng hạn khi sử dụng UIImagePickerController, ngoài việc quan tâm tới các hình ảnh mà người dùng chọn (stream), ứng dụng cần tương tác với chính UIImagePickerController để ẩn, hiển, … như vậy không thể bọc UIImagePickerController bên trong Observable. Khi đó, Subject sẽ đóng vai trò cầu nối, giúp chuyển đổi các tương tác của người dùng thành các streams tương ứng.

Lưu ý: Thư viện RxCocoa đã cung cấp rất nhiều extension cho các UIControl, nên trước khi sử dụng Subject, hãy kiểm tra lại những extension trong RxCocoa.

1.1. PublishSubject

Chúng ta cùng xem xét một ví dụ sử dụng PublishSubject để chuyển đổi hình ảnh được người dùng chọn thành một stream.

//
// ViewController.swift
// RxSubject
//
// Created by Hung Dinh Van on 12/27/16.
// Copyright © 2016 ChuCuoi. All rights reserved.
//
import UIKit
import RxSwift
import RxCocoa
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
button.rx.tap.subscribe(onNext: { [unowned self] in
imagePicker.allowsEditing = false
imagePicker.sourceType = .photoLibrary
self.present(imagePicker, animated: true, completion: nil)
}).addDisposableTo(disposeBag)
didFinishPickingSubject.subscribe(onNext: { [unowned self] info in
if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
self.imageView.contentMode = .scaleAspectFit
self.imageView.image = pickedImage
}
self.dismiss(animated: true, completion: nil)
}).addDisposableTo(disposeBag)
}
@IBOutlet fileprivate weak var imageView: UIImageView!
@IBOutlet fileprivate weak var button: UIButton!
fileprivate let disposeBag = DisposeBag()
fileprivate let didFinishPickingSubject = PublishSubject<[String : Any]>()
}
extension ViewController: UIImagePickerControllerDelegate, UINavigationControllerDelegate {
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
didFinishPickingSubject.onNext(info)
}
}

Một đặc điểm của PublishSubject là các phần tử có thể được phát ngay sau khi Subject được khởi tạo, bất chấp chưa có đối tượng nào subscribe tới nó (hot observable). Observer sẽ không nhận được các phần tử phát ra trước thời điểm subscribe. Tương tự, Observer cũng không nhận được các phần tử được phát ra sau khi có tín hiệu lỗi. Ví dụ sau kết quả sẽ in ra: 2, 3. Bởi vì 1 phát ra trước khi subscribe và 4 được phát ra sau khi error.

enum SubjectError: Error {
case unknown
}
let subject = PublishSubject<Int>()
subject.onNext(1)
subject.subscribe(onNext: { value in
print(value)
}).addDisposableTo(disposeBag)
subject.onNext(2)
subject.onNext(3)
subject.onError(SubjectError.unknown)
subject.onNext(4)

view raw
PublishSubject.swift
hosted with ❤ by GitHub

1.2. BehaviorSubject

BehaviorSubject có cơ chế hoạt động gần giống với PublishSubject, nhưng Observer sẽ nhận được giá trị mặc định hoặc giá trị ngay trước thời điểm subscribe. Observer sẽ nhận được ít nhất một giá trị.

Chẳng hạn, nếu coi việc cuộn thanh trượt của UIScrollView là một stream (offset là giá trị của các phần tử trong stream), thì ngay khi subscribe vào stream, chúng ta cần biết vị trí offset hiện tại của UIScrollView, do vậy chúng ta cần sử dụng BehaviorSubject

let contentOffsetSubject = BehaviorSubject<CGPoint>(scrollView.contentOffset)
func scrollViewDidScroll(_ scrollView: UIScrollView) {
contentOffsetSubject.on(.next(scrollView.contentOffset))
}

s-behaviorsubject

Để khởi tạo BehaviorSubject, chúng ta cần cung cấp giá trị mặc định (phần tử màu hồng trên sơ đồ).

Observer 1 subscribe khi chưa có phần tử nào được phát ra, khi đó nó sẽ nhận được giá trị mặc định và những giá trị sau thời điểm subscribe (màu đỏ, xanh lá, xanh lam).

Observer 2 subscribe sau khi các phần tử màu đỏ và xanh lá được phát, nên sẽ nhận được phần tử màu xanh (ngay trước thời điểm subscribe) và phần tử màu xanh

1.3. ReplaySubject

ReplaySubject tương tự như BehaviorSubject nhưng thay phát thêm duy nhất một phần tử trước đó, ReplaySubject cho phép developer chỉ định số lượng phần tử tối đa được phát lại khi subscribe. Ngoài ra, khi khởi tạo ReplaySubject, chúng ta không cần khai báo giá trị mặc định như BehaviorSubject.

let subject = ReplaySubject<Int>.create(bufferSize: 5)
subject.onNext(1)
subject.onNext(2)
subject.onNext(3)
subject.onNext(4)
subject.onNext(5)
subject.onNext(6)
subject.subscribe(onNext: { value in
print(value)
}).addDisposableTo(disposeBag)
subject.onNext(7)

Kết quả in ra 2, 3, 4, 5, 6, 7, bao gồm 5 phần trước khi subscribe và một phần tử sau khi subscribe. Lưu ý: ở ví dụ này ReplaySubject sẽ phát lại tối đa 5 phần tử. Giả sử chúng ta chỉ phát ra 3 phần tử trước khi subscribe, thì nó vẫn được phát lại mà không chờ đủ 5 phần tử.

Ở ví dụ trên nếu có error ngay sau khi phát phần tử 4 chẳng hạn thì kết quả là: 1, 2, 3, 4 và stream bị kết thúc.

1.4. Sử dụng Subject nâng cao

Với các Control mà RxCocoa không hỗ trợ, các bạn có thể sử dụng Subject trực tiếp như các ví dụ trên. Nhưng việc sử dụng như trên không có tính tái sử dụng. Ngoài ra, chúng ta vẫn phải khai báo các delegate như cách thông thường. Trong buổi meetup gần nhất về Rx, mình nhận được một câu hỏi là: Có cách nào sử dụng Rx cho UIImagePickerController(và các control không được RxCocoa hỗ trợ) một cách Reactive không?

Câu trả lời là các bạn hoàn toàn có thể hiện thực những extension giống RxCocoa như ví dụ sau. Với cách này, các bạn có thể tái sử dụng những extension này ở mọi nơi, không cần hiện thực các delegate khi sử dụng.

let imagePicker = UIImagePickerController()
imagePicker.rx.didFinishPickingMediaWithInfo.subscribe(onNext: { [unowned self] info in
if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
self.imageView.contentMode = .scaleAspectFit
self.imageView.image = pickedImage
}
self.dismiss(animated: true, completion: nil)
}).addDisposableTo(disposeBag)
imagePicker.rx.didCancel.subscribe(onNext: { [unowned self] in
print("Cancelled")
self.dismiss(animated: true, completion: nil)
}).addDisposableTo(disposeBag)

Trước hết, định nghĩa một DelegateProxy. Phần quan trọng nhất là thực hiện các hàm của UIImagePickerControllerDelegate

//
// RxImagePickerControllerDelegateProxy.swift
// RxCocoa
//
// Created by Hung Dinh Van on 12/27/2016.
// Copyright © 2016 ChuCuoi. All rights reserved.
//
import Foundation
import RxSwift
import RxCocoa
import UIKit
/// For more information take a look at `DelegateProxyType`.
class RxImagePickerControllerDelegateProxy
: DelegateProxy
, UIImagePickerControllerDelegate
, UINavigationControllerDelegate
, DelegateProxyType {
fileprivate var _didFinishPickingSubject: PublishSubject<[String : Any]>?
fileprivate var _didCancelPickingSubject: PublishSubject<Void>?
weak fileprivate(set) var imagePickerController: UIImagePickerController?
var didFinishPickingSubject: Observable<[String : Any]> {
if _didFinishPickingSubject == nil {
_didFinishPickingSubject = PublishSubject<[String : Any]>()
}
return _didFinishPickingSubject!
}
var didCancelPickingSubject: Observable<Void> {
if _didCancelPickingSubject == nil {
_didCancelPickingSubject = PublishSubject<Void>()
}
return _didCancelPickingSubject!
}
/// Initializes `RxImageViewControllerDelegateProxy`
/// – parameter parentObject: Parent object for delegate proxy.
required init(parentObject: AnyObject) {
self.imagePickerController = (parentObject as! UIImagePickerController)
super.init(parentObject: parentObject)
}
// MARK: delegate methods
/// For more information take a look at `DelegateProxyType`.
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let subject = _didFinishPickingSubject {
subject.on(.next(info))
}
self._forwardToDelegate?.imagePickerController?(picker, didFinishPickingMediaWithInfo: info)
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
if let subject = _didCancelPickingSubject {
subject.on(.next())
}
self._forwardToDelegate?.imagePickerControllerDidCancel?(picker)
}
// MARK: delegate proxy
/// For more information take a look at `DelegateProxyType`.
override class func createProxyForObject(_ object: AnyObject) -> AnyObject {
let imagePickerController = (object as! UIImagePickerController)
return castOrFatalError(imagePickerController.createRxDelegateProxy())
}
/// For more information take a look at `DelegateProxyType`.
class func setCurrentDelegate(_ delegate: AnyObject?, toObject object: AnyObject) {
let imagePickerController: UIImagePickerController = castOrFatalError(object)
imagePickerController.delegate = castOptionalOrFatalError(delegate)
}
/// For more information take a look at `DelegateProxyType`.
class func currentDelegateFor(_ object: AnyObject) -> AnyObject? {
let imagePickerController: UIImagePickerController = castOrFatalError(object)
return imagePickerController.delegate
}
deinit {
if let finishSubject = _didFinishPickingSubject {
finishSubject.on(.completed)
}
if let cancelSubject = _didCancelPickingSubject {
cancelSubject.on(.completed)
}
}
}
func castOrFatalError<T>(_ value: Any!) -> T {
let maybeResult: T? = value as? T
guard let result = maybeResult else {
fatalError("Failure converting from \(value) to \(T.self)")
}
return result
}
func castOptionalOrFatalError<T>(_ value: Any?) -> T? {
if value == nil {
return nil
}
let v: T = castOrFatalError(value)
return v
}

Phần tiếp theo, định nghĩa Rx extensions cho UIImagePickerController. Công việc chính là public các Observable của Proxy phía trên

//
// UIImagePickerController+Rx.swift
// RxSubject
//
// Created by Hung Dinh Van on 12/27/16.
// Copyright © 2016 ChuCuoi. All rights reserved.
//
import UIKit
import RxCocoa
import RxSwift
extension UIImagePickerController {
/// Factory method that enables subclasses to implement their own `delegate`.
/// – returns: Instance of delegate proxy that wraps `delegate`.
func createRxDelegateProxy() -> RxImagePickerControllerDelegateProxy {
return RxImagePickerControllerDelegateProxy(parentObject: self)
}
}
extension Reactive where Base: UIImagePickerController {
/// Reactive wrapper for `delegate`.
///
/// For more information take a look at `DelegateProxyType` protocol documentation.
var delegate: DelegateProxy {
return RxImagePickerControllerDelegateProxy.proxyForObject(base)
}
/// Reactive wrapper for `didFinishPickingMediaWithInfo`.
var didFinishPickingMediaWithInfo: Observable<[String : Any]> {
let proxy = RxImagePickerControllerDelegateProxy.proxyForObject(base)
return proxy.didFinishPickingSubject
}
/// Reactive wrapper for `didCancel`.
var didCancel: Observable<Void> {
let proxy = RxImagePickerControllerDelegateProxy.proxyForObject(base)
return proxy.didCancelPickingSubject
}
}

Xong. Chúng ta hoàn toàn có sử dụng UIImagePickerController như các extension khác của RxCocoa.

3. Kết

Mình vừa giới thiệu về Subject và Variable trong RxSwift. Chúng là những cầu nối giữa mô hình Imperative với Declarative trong RxSwift. Vì thế, chúng ta nên sử dụng Subject đúng như mục đích nó sinh ra. Khi sử dụng Subject nó có nhược điểm sau:

  • Việc cho phép onNext tại nhiều nơi thay vì tập trung tại trong nội tại như Observable khiến khó maintain hơn
  • Không đảm bảo thread-safety: nếu onNext được gọi trên các thread khác nhau sẽ vi phạm Observable Contract
  • Hot Observable: Observer có thể bỏ qua một số phận tử trước khi subscribe (không nên sử dụng các toán tử max, min, sum, … cho Subject)

Tóm lại, khi nào nên sử dụng Subject? Câu trả lời: Khi không có lựa chọn khác.

 

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Đăng xuất /  Thay đổi )

Google photo

Bạn đang bình luận bằng tài khoản Google Đăng xuất /  Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Đăng xuất /  Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Đăng xuất /  Thay đổi )

Connecting to %s