RxSwift by Examples #2 – Observable and the Bind
Bài đăng này đã không được cập nhật trong 3 năm
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ề Observables
và Observers
ở phần trước, phần này ta sẽ tìm hiểu về:
Subjects
:Observable
vàObserver
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ởisubject
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 đượcPublishSubject
: 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 subscribeReplaySubject
: 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àobuffer size
củaReplaySubject
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()
và .onComplete()
). Tuy nhiên, Variable
sẽ tự động gửi event .onComplete()
khi nó được deallocated.
###Example
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ànhUIColor
. 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 optionalCGPoint
? 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:
All rights reserved