RxSwift by Examples #2 – Observable and the Bind

Dựa theo tài liệu từ TheDroidSonroids Và tiếp theo từ phần trước: Part I ###Definitions Ở phần I chúng ta đã nói về những thứ có bản của RxSwift và RxCocoa, trong phần này chúng ta sẽ nói về bindings. Binding đơn giản chỉ có ý nghĩa là kết nối, và chúng ta sẽ kết nối Observables với Subjects. Có 1 số thuật ngữ mà chúng ta chưa từng biết đến, nên trước khi bắt đầu chúng ta cần biết về 1 số định nghĩa. CHúng ta đã nói về ObservablesObservers ở phần trước, phần này ta sẽ tìm hiểu về:

  • Subjects: ObservableObserver cùng 1 lúc. Về cơ bản nó có thể quan sát và bị quan sát =))
  • BehaviorSubject: khi bạn subscribe đến nó, bạn sẽ nhận được giá trị cuối cùng được phát ra bởi subject và tiếp tục là những giá trị phát ra sau khi bạn subscribe, bạn cũng sẽ nhận được
  • PublishSubject: khi bạn subscribe đến nó, bạn sẽ chỉ nhận được những giá trị mà nó phát ra sau khi bạn subscribe
  • ReplaySubject: khi bạn subscribe đến nó, bạn sẽ nhận được giá trị phát ra sau khi subscribe, đồng cả những giá trị mà đã được phát ra trước khi subscribe. Vậy thì bạn sẽ nhận được bao nhiêu gía trị cũ? Nó tuỳ thuộc vào buffer size của ReplaySubject mà bạn subscribe tới, bạn quyết định số lượng đó là bao nhiêu khi bạn init subject.

Để mọi thứ dễ hiểu hơn, hãy tưởng tượng hôm nay là cưới của bạn (lol). Sau khi tổ chức xong xuôi, màn đêm buông xuống, bạn sẽ làm gì? động phòng hoa trúc? Sai, bạn sẽ phải đi kiểm lại phong bì cưới. Bạn bắt đầu mở dần phong bì kiểm tra xem mấy thằng bạn cứt mừng cưới mình bao nhiêu: 1, rồi 2, rồi 3 cái. Đột nhiên, vợ bạn vọt ra từ nhà tắm và như 1 người vợ tử tế, cô ấy cần phải biết bạn có được bao nhiêu phong bì (lol). Vậy là bạn phải nói cho cô ấy nghe. Trong thế giới của Rx, bạn sẽ gửi observable sequence (phong bì) cho observer (vợ bạn). Điều thú vị là mặc dù cô ấy bắt đầu quan sát bạn sau khi bạn đã phát ra 1 số giá trị rồi, nhưng cô ấy vẫn biết được tất cả thông tin. Với cô ấy, chúng ta sẽ có 1 ReplaySubject với buffer = 3. (Chúng ta lưu lại 3 cái phong bì được mở ra cuối cùng và đưa nó ra mỗi khi có 1 subsciber mới) =))

Bạn vẫn tiếp tục mở phong bì, thì bố và mẹ bạn xuất hiện và mong muốn được biết về việc bạn kiếm được bao nhiêu tiền =)). Dĩ nhiên bạn ko muốn điều đó và giấu hoàn toàn số phong bì bạn đã mở trước đó. Bố bạn ko hề quan tâm đến điều đó, ông chỉ ngồi xuống và xem bạn xé phong bì. Ông chính là kiểu PublishSubject, chỉ nhận được giá trị mà phát ra sau khi subscription. Còn mẹ bạn, vì thân thiết hơn nên bạn cho bà biết cả về cái phong bì cuối cùng mà bạn mở là của cậu Viện => bà chính là BehaviorSubject.

Và còn 1 thứ quan trọng nữa được gọi là Variable: Nó là wrapper của BehaviorSubject. Khi sử dụng Variable bạn chỉ có thể submit event .onNext() (khi sử dụng BehaviorSubject bạn có thể trực tiếp gửi các event .onError().onComplete()). Tuy nhiên, Variable sẽ tự động gửi event .onComplete() khi nó được deallocated.

###Example demo Chúng ta sẽ code 1 app đơn giản: connect màu sắc của quả bóng với vị trị của nó trong view và cũng kết nối màu sách của background với màu sắc của quả bóng. Đầu tiên, hãy tạo 1 app mới, chúng ta vẫn sẽ sử dụng RxSwift và RxCocoa, cùng với Chameleon để connect màu sắc dễ dàng. Podfile sẽ trông như sau:

platform :ios, '9.0'
use_frameworks!
 
target 'ColourfulBall' do
 
pod 'RxSwift'
pod 'RxCocoa'
pod 'ChameleonFramework/Swift', :git => 'https://github.com/ViccAlexander/Chameleon.git'
 
end
 
post_install do |installer|
    installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
              config.build_settings['ENABLE_TESTABILITY'] = 'YES'
              config.build_settings['SWIFT_VERSION'] = '3.0'
        end
    end
end

Bây giờ thì code thôi: đầu tiên, ta sẽ vẽ 1 hình tròn trong main view:

import ChameleonFramework
import UIKit
import RxSwift
import RxCocoa
 
class ViewController: UIViewController {
 
    var circleView: UIView!
    
    override func viewDidLoad() {
        super.viewDidLoad()
        setup()
    }
 
    func setup() {
        // Add circle view
        circleView = UIView(frame: CGRect(origin: view.center, size: CGSize(width: 100.0, height: 100.0)))
        circleView.layer.cornerRadius = circleView.frame.width / 2.0
        circleView.center = view.center
        circleView.backgroundColor = .green
        view.addSubview(circleView)
    }
}

Tiếp theo, để di chuyển được quả bóng, chúng ta cần phải sử dụng UIPanGestureRecognizer để điều khiển:

func setup() {
    // Add circle view
    circleView = UIView(frame: CGRect(origin: view.center, size: CGSize(width: 100.0, height: 100.0)))
    circleView.layer.cornerRadius = circleView.frame.width / 2.0
    circleView.center = view.center
    circleView.backgroundColor = .green
    view.addSubview(circleView)
        
    // Add gesture recognizer
    let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(circleMoved(_:)))
    circleView.addGestureRecognizer(gestureRecognizer)
}
 
func circleMoved(_ recognizer: UIPanGestureRecognizer) {
    let location = recognizer.location(in: view)
    UIView.animateWithDuration(0.1) {
        self.circleView.center = location
    }
}

Bây giờ, khi run app bạn sẽ thấy như sau: Việc tiếp theo của chúng ta sẽ là bind: connect vị trí của quả bóng với màu sắc của nó: đầu tiên chúng ta cần phải observe vị trí của quả bóng bằng cách dùng rx.observe() và truyền dữ liệu đó vào 1 Variable bằng cách sử dụng bindTo(). Nhưng cái gì sẽ binding trong trường hợp này? Mỗi khi 1 position mới được phats ra bởi quả bóng, variable sẽ nhận một signal mới về nó. Trong trường hợp này variable sẽ là 1 Observer vì nó quan sát position của quả bóng. Chúng ta sẽ tạo variable này bên trong ViewModel - đc sử dụng để tính toán UI: cụ thể là mỗi khi variable nhận đc position mới, chúng ta sẽ tính toán màu sắc mới cho quả bóng. ViewModel của chúng ta rất đơn giản, chỉ bao gồm 2 properties: centerVariable đc sử dụng như là observer&observable - chúng ta sẽ lưu dữ liệu vào đó và lấy từ đó ra. Thứ 2 là backgroundColorObservable nó ko phải là variable, chỉ là Observable Bạn sẽ thắc mắc vì sao centerVariable là 1 Variable nhưng backgroundColorObservable chỉ là Observable? Rõ ràng là centerVariable được kết nối với position của quả bóng, tức là nó quan sát position của quả bóng và được thay đổi mỗi khi position của quả bóng thay đổi => vì thế nó là 1 observer. Đồng thời, bên trong ViewModel chúng ta sử dụng centerVariable như một observable nên nó vừa là observer vừa là observable => dẫn đến nó là Subject. Và bởi vì chúng ta muốn chắc chắn lấy đc giá trị cuối cùng của position của quả bóng => nên centerVariable là 1 Variable (wrapper của BehaviourSubject) chứ ko phải là PublishSubject hay ReplaySubject. backgroundColorObservable thì chỉ là Observable bởi vì nó không thay đổi theo 1 cái gì cả. ViewModel của chúng ta sẽ trông như sau:

import ChameleonFramework
import Foundation
import RxSwift
import RxCocoa
 
class CircleViewModel {
    
    var centerVariable = Variable<CGPoint?>(.zero) // Create one variable that will be changed and observed
    var backgroundColorObservable: Observable<UIColor>! // Create observable that will change backgroundColor based on center
    
    init() {
        setup()
    }
 
    setup() {
    }
}

Tiếp theo chúng ta sẽ setup backgroundColorObservable. Chúng ta muốn nó thay đổi dựa trên mỗi giá trị CGPoint mới mà centerVariable phát ra:

func setup() {
    // When we get new center, emit new UIColor
    backgroundColorObservable = centerVariable.asObservable()
        .map { center in
            guard let center = center else { return UIColor.flatten(.black)() }
            
            let red: CGFloat = ((center.x + center.y) % 255.0) / 255.0 // We just manipulate red, but you can do w/e
            let green: CGFloat = 0.0
            let blue: CGFloat = 0.0
            
            return UIColor.flatten(UIColor(red: red, green: green, blue: blue, alpha: 1.0))()
        }
}

step by step:

  • Chuyển variable thành observable - vì variable nghĩa là cả observer và observable
  • Map mỗi giá trị mới của CGPoint thành UIColor. Chúng ta nhận đc giá trị postion mới mà observable của chúng ta cung cấp, rồi dựa vào 1 số tính toán khoa học phức tạp =)) để tạo ra1 UIColor mới.
  • Bạn có thể chú ý rằng: Observable của chúng ta là 1 optional CGPoint? Tại sao? Chúng ta sẽ tìm hiểu sau, tuy nhiên vì vậy nên chúng ta sẽ phải return default color trong trường hợp nó bị nil

Như vậy chúng ta đã có 1 Observable mà sẽ phát ra tín hiệu background color cho quả bóng. Ta chỉ cần update quả bóng dựa trên giá trị đó. điều đó vô cùng dễ dàng với việc subscribe() tới Observable

// Subscribe to backgroundObservable to get new colors from the ViewModel.
circleViewModel.backgroundColorObservable
    .subscribe(onNext: { [weak self] backgroundColor in
        UIView.animateWithDuration(0.1) {
            self?.circleView.backgroundColor = backgroundColor
            // Try to get complementary color for given background color
            let viewBackgroundColor = UIColor(complementaryFlatColorOf: backgroundColor)
            // If it is different that the color
            if viewBackgroundColor != backgroundColor {
                // Assign it as a background color of the view
                // We only want different color to be able to see that circle in a view
                self?.view.backgroundColor = viewBackgroundColor
            }
        }
    })
    .addDisposableTo(disposeBag)

Như bạn thấy, chúng ta thay đổi màu background của view bằng màu bổ sung của màu của quả bóng. Phần code này bạn nên đặt trong setup(), và nó sẽ như sau:

func setup() {
    // Add circle view
    circleView = UIView(frame: CGRect(origin: view.center, size: CGSize(width: 100.0, height: 100.0)))
    circleView.layer.cornerRadius = circleView.frame.width / 2.0
    circleView.center = view.center
    circleView.backgroundColor = .green
    view.addSubview(circleView)
    
    circleViewModel = CircleViewModel()
    // Bind the center point of the CircleView to the centerObservable
    circleView
        .rx.observe(CGPoint.self, "center")            
        .bindTo(circleViewModel.centerVariable)
        .addDisposableTo(disposeBag)
 
    // Subscribe to backgroundObservable to get new colors from the ViewModel.
    circleViewModel.backgroundColorObservable
        .subscribe(onNext: { [weak self] backgroundColor in
            UIView.animateWithDuration(0.1) {
                self?.circleView.backgroundColor = backgroundColor
                // Try to get complementary color for given background color
                let viewBackgroundColor = UIColor(complementaryFlatColorOf: backgroundColor)
                // If it is different that the color
                if viewBackgroundColor != backgroundColor {
                    // Assign it as a background color of the view
                    // We only want different color to be able to see that circle in a view
                    self?.view.backgroundColor = viewBackgroundColor
                }
            }
        })
        .addDisposableTo(disposeBag)
    
    let gestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(circleMoved(_:)))
    circleView.addGestureRecognizer(gestureRecognizer)
}

Kết quả cuối cùng của bạn: