+3

RxSwift - Oservable và Binding

Ta đã được biết qua về khái niệm observables và observers trong bài viết trước về RxSwift , tiếp theo, hãy cùng tìm hiểu các khái niệm khác:

  • Subject: Là oservale và observer cùng lúc, cơ bản nó có thể observe và được observe.
  • Behavior subject: Khi subscribe đối tượng này, ta sẽ lấy được giá trị mới nhất được phát ra bởi object, và các giá trị khác được phát ra sau khi đăng ký
  • Public subject: Khi subscribe đối tượng này, ta chỉ có thể lấy được giá trị được phát ra sau khi đăng ký
  • Replay subject: Khi subscribe đối tượng này, ta có thể lấy được giá trị được phát ra sau khi đăng ký, và cả giá trị trước khi đăng ký.

Ngoài ra còn một đối tượng khác được gọi là variable, nó bao bọc behavior subject. Ta chỉ có thể gởi đi .onNext() event, khi sử dụng behavior subject, ta được phép truy cập trực tiếp tới các function như .onError(), .onCompleted(). Và variable sẽ tự động gởi .onCompleted() event khi nó bị huỷ.

Hãy cũng tìm hiểu ví dụ sau để hiểu rõ hơn nhé. Ta sẽ cùng tạo ra một ứng dụng đơn giản giúp kết nối quả bóng với vị trí trong view và kết nối background color của view với color của quả bóng.

Đầu tiên, hãy tạo mới project và đặt tên là ColourfulBall, cài đặt RxSwift, RxCocoa, Chameleon để kết nối các màu trong CocoaPods 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

Tiếp theo, vẽ một vòng trong trong main view của viewcontroller. Bạn có thể viết code trực tiếp hoặc sử dụng storyboard đều được:

import ChameleonFramework
import UIKit
import RxSwift
import RxCocoa
 
class CircleViewController: 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)
    }
}

Sau đó, add pan gesture vào quả bóng, với mục đích sẽ di chuyển quả bóng theo vị trí mà ngón tay kéo trên màn hình.

// 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
    }
}

Tiếp theo ta sẽ kết nối vị trí của quả bóng với quả bóng color. Để làm được việc này, ta cần observe vị trí trung tâm quả bóng bằng cách sử dụng hàm rx.observe() và sau đó bind nó tới một Variable, sử dụng bindTo(). Việc binding này sẽ giúp mỗi lần quả bóng phát ra một vị trí mới, variable sẽ nhận được một tín hiệu về nó. Trong trường hợp này, variable mà ta nhắc tới chính là observer, vì nó sẽ observe vị trí của quả bóng.

Ta sẽ tạo ra variable này trong một ViewModel, nó được dùng để tính toán UI. Mỗi lần variable này nhận được một vị trí mới, ta sẽ tính toán và set lại background color mới cho quả bóng. Trong ViewModel này, hãy khai báo hai thuộc tính:

  • centerVariable: chính là observer và oservable, ta sẽ lưu dữ liệu vào đây.
  • backgroundColorObservable: đây là một obsevable

Bạn có lẽ sẽ thắc mắc tại sao centerVariable lại là một variable, trong khi backgroundColorObservable là một observable. Như ta thấy, center của quả bóng được kết nối với centerVariable, có nghĩa là mỗi lần center thay đổi, centerVariable cũng sẽ thay đổi theo, vì vậy nó là observer. Và trong ViewModel này, centerVariable được sử dụng như một observable, nên nó vừa là observer, vừa là observable. Và nó là variable chứ không phải public subject hay replay subject, vì ta muốn lấy giá trị mới nhất của center quả bóng mỗi lần subscribe nó. Còn backgroundColorObservable là một observable, vì nó không bao giờ bị ràng buộc bởi một đối tượng nào khác.

Dưới đây là khai báo của ViewModel:

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() {
    }
}

Bây giờ, ta sẽ thiết lập backgroundColorObservable, nó sẽ thay đổi giá trị dựa trên CGPoint được lấy từ centerVariable.

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))()
        }
}

Trong đoạn code trên, ta đã thực hiện các công việc sau:

  • Truyền variable vào observable, vì variable có thể là observer và observable, ta cần quyết định xem nó sẽ là gì. Trong trường hợp này, ta muốn observe nó, nên sẽ chuyển nó thành observable.
  • Map tất cả các giá trị mới của CGPoint sang UIColor. Ta sẽ lấy được giá trị center mới mà observerable tạo ra, sau đó tạo ra một màu mới dựa vào tính toán toán học.
  • Bạn sẽ nhận thấy rằng observale của ta là một optional CGPoint. Việc này sẽ giúp tránh trường hợp nil xảy ra.

Bây giờ, đã có một observable sẽ phát ra background color mới cho quả bóng, ta chỉ cần update lại màu của quả bóng với giá trị mới này:

// 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)

Ta vừa thêm sự thay đổi cho background color cho view chính của màn hình để phân biệt và dễ dàng nhận ra bóng. Function setup() sau cùng 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)
}

Đến đây, chúng ta đã hoàn thành việc thay đổi màu sắc mà không cần dùng đến delegates, notification và tất cả những đoạn code soạn sẵn cho công việc này như trước đây. Dưới đây là kết quả mà ta đã đạt được, hy vọng mọi người có thể hiểu thêm về ngôn ngữ tuyệt vời này.


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí