RxSwift by Examples #2 – Observable and the Bind
Edit 18.01.2017: This post was updated to Swift 3.0 and RxSwift 3.1
In the first chapter we’ve learned the basics about RxSwift and RxCocoa (if you haven’t seen it yet, I really encourage you to do so!) . The time has come and we will expand our knowledge in a reactive way. Today we will talk about bindings.
Don’t worry, binding just means connecting and we will connect our Observables with Subjects. There is some terminology that we haven’t learned before, so…
Definitions
Before we start we need to get in touch with some definitions. We learned about Observables and Observers and today we will learn about other types.
Subject – Observable and Observer at once. Basically it can observe and be observed.
BehaviorSubject – When you subscribe to it, you will get the latest value emitted by the Subject, and then the values emitted after the subscription.
PublishSubject – When you subscribe to it, you will only get the values that were emitted after the subscription.
ReplaySubject – When you subscribe to it, you will get the values that were emitted after the subscription, but also values that were emitted before the subscription. How many old values will you get? It depends on the buffer size of ReplaySubject you subscribe to. You specify it in init of the Subject.
Okay, okay. Too many subjects. Let’s simplify it a little bit. You are having a birthday party ? and you are opening the presents you’ve got.
You’ve opened first, second, third gift. And whoops! Your mom was cooking some delicious food and is late to the opening party. As a mom, she just has to know what presents you’ve got already. So you tell her about them. In Rx world you’ve sent observable sequence (presents) to the observer (your mom). What’s interesting is that she started observing you after you already emitted few values, but she got whole info anyways. For her we are a ReplaySubject with buffer = 3 (we save 3 latest presents and give it every time a new subscriber appears).
You are still opening presents and there you see that two of your friends (Jack and Andy) were also late to the party. Jack happens to be your close friend so he asks what have you opened so far. As you are kinda angry that he missed a part of it, you tell him only the latest present you’ve opened. He doesn’t know that there were more of it, so he is happy with it. In Rx world you’ve sent only latest emitted value to the observer (Jack). He will also get the next values as you emit them (next presents you will open). For him we are a BehaviorSubject (we kinda changed the Subject ?).
There is also Andy, who happens to be just a friend and doesn’t really care about the presents you’ve opened so he just sits down and waits for the rest of the show. As you can imagine, for him we are just a PublishSubject. He just gets values that are emitted after the subscription.
There is also something called Variable. This is wrapper around BehaviorSubject. The thing is, you can only submit the .onNext()
event (when using BehaviorSubject you have direct access to sending .onError()
, .onCompleted()
). Also, Variable automatically sends .onCompleted()
event when it’s being deallocated.
Alright, enough with definitions. Let’s try it out!
Example
We will create simple app that will connect ball color with position in view and also we will connect view’s background color with the ball color.
First let’s create a project as we created in a tutorial before. We will also use CocoaPods and in addition to RxSwift and RxCocoa we will use Chameleon to nicely connect the colors. Our Podfile should look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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 |
After setting up the project we can start coding! First we will draw circle in the main view of our controller. We will do it from code, but if you want to do it in Interface Builder – you are free to go. Example of creating that view looks like one below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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) } } |
That code should be self-explanatory (we just created rounded UIView) so we will just move forward. Next step would be to move our ball on pan gesture. In order to do that let’s add UIPanGestureRecognizer
and change the frame of it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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 } } |
Perfect! Our app should now look somewhat similar to the one below:
The next step would be to bind something! Let’s connect position of the ball with ball’s color. How to do that? First we will observe ball’s center position using rx.observe()
and then bind it to a Variable, using bindTo()
. But what does binding do in our case? Well, every time a new position is emitted by our ball, the variable will receive a new signal about it. In this case our variable is an Observer, because it will observe the position.
We will create this variable in a ViewModel, which will be used to calculate UI things. In this case every time our variable will get a new position, we will calculate new ball’s background color. Easy, right?
LET’S TALK ABOUT YOUR APP
We’re 100% office based team with 7-years’ experience
in mobile & web app development
Now we need to create our ViewModel. It will be a really simple one, because we will have only 2 properties: centerVariable
which will be our observer & observable – we will save data to it and we will get it. And the second one will be backgroundColorObservable
. It is actually not a Variable, but only an Observable.
Now you might ask “Why is centerVariable
a Variable, but backgroundColorObservable
is an Observable?” And that is a brilliant question! See, our observable center of ball is connected with centerVariable
. It means that overtime the center changes, centerVariable
will get the change. It is then an Observer. Also in our ViewModel we use centerVariable as an Observable, which makes it both Observer and Observable which is just a Subject. Why Variable and not PublishSubject, ReplaySubject? Because we want to be sure we will get the latest center of that ball every time we subscribe to it.
backgroundColorObservable
is just an Observable, it is never bound to anything so it makes perfect sense to leave it just as an Observable.
Alright! Done with the theory, let’s code! Our basic ViewModel should look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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! // Create observable that will change backgroundColor based on center init() { setup() } setup() { } } |
Perfect. Now we need to setup our backgroundColorObservable
. We want it to change based on new CGPoint
produced by centerVariable
.
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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:
- Transform our variable into Observable – since Variable can be both Observer and Observable, we need to decide which one is it. And since we want to observe it, we transform it into Observable.
- Map every new value of
CGPoint
toUIColor
. We get the new center that our Observable produced, then based on (not-so) really complicated math calculations we create newUIColor
. - You may notice that our Observable is an optional
CGPoint
. Why? We will explain it in a second. But we need to protect ourselves and in case we get nil, return some default color (black in our case).
Okay. We are really close to the end. We have now Observable that will emit new background color for a ball. We just need to update our ball based on the new values. Now that’s really easy. It’s similar to our part #1 of the series. We will subscribe()
to the Observable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 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) |
As you can see we’ve also added changing the background color of our view to the complementary color of our ball. Also we have the check if complementary color is the same as the balls’ color (we want to see it at least!). But that’s a feature, not that main task. You need to add this code rather in the setup()
method, so it looks similar to:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | 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) } |
And you’re done! The whole task of manipulating colors without delegates, notifications and that whole boilerplate code we always use for that type of tasks. The result should be somewhat similar to the one from the beginning of the Example.
Now you can try to customize it! Maybe add binding between center and the ball size? Then try to change its cornerRadius
based on its width
and height
? It’s really up to you but I think that with Rx that tasks are really delightful.
That’s it for today and as always visit our GitHub for whole project and see you guys next time!
You can find complete source code on Droids on Roids’s GitHub repository and here you can check other RxSwift examples!
P.S. I try to implement more examples on a weekly basis so subscribe(?)
to our RxSwiftExamples repo!
Read more articles about RxSwift
RxSwift by Examples #1 – The Basics
RxSwift by Examples #2 – Observable and the Bind
RxSwift by Examples #3 – Networking
RxSwift by Examples #4 – Multithreading
About the author
Ready to take your business to the next level with a digital product?
We'll be with you every step of the way, from idea to launch and beyond!
I’ve never tried animations with RxSwift. Looks cool. Thanks for that!
Sky is the limit! Thank you! ☺️
Great article! But you didn’t explain why the Observable is an optional CGPoint
Oh right… Basically because of the fact that when you do rx.observe you can get nil or a value, due to that fact that the string you provide as a key path might not be available.
I could put all code to circleMoved(), which means I could do all in Tap Gesture selector function — then what’s the good of using RxSwift here?
Well, to be frank, the whole RxSwift is built in on top of currently implemented features, so you could say that about everything if you want to. Here we show how to implement a feature using model, to extract the logic from ViewController. Then you can see how to bind things together using RxSwift.
Yeah yeah, I understand the point. I just feel it is not as simple as expected using RxSwift here.
Thanks for the article!
Why didn’t we do something like this:
circleView
.rx_observe(CGPoint.self, “center”)
.subscribeNext{ [weak self] point in
// convert CGPoint value to UIColor
// for self.view.backgroundColor
}
.addDisposableTo(disposeBag)
Hey! Thanks for the question. Of course we could do that, but with the way I presented you can easily unit test the model, because you have separated logic.
Wow, it was quick 🙂
Good approach, thanks!
Thanks much for the series! Fun to read, easy to follow. Found a small bug in the code: the circleMoved function requires an unnamed argument in order to work with the selector. Cheers!
Hey Michał, thank you very much, really appreciate the feedback 🙂 About the bug, you are right, sorry about that, really easy to overlook something with the upgrade to Swift 3.0, so thank you for that as well!
Thanks for the tutorial
But I got a problem when following this example, I found out when I move the color ball, the center positon is not sent to centerVariable…
Could you give me some clues?
Thank you 🙂
Hey @disqus_v09j6kGHxr:disqus, does the example work for you? Do you have troubles implementing it on your own? If yes, we would need some code for that 🙂
Hey Mroz,
Could you please have a look at my viewController file:
https://github.com/Wei1988/RxSwiftDemos/blob/master/ViewControllers/ObservableBindingViewController.swift
I think my problem is that I cannot observe the center position changes..
Thank you 🙂
Hi Mroz,
Finally, I find out why.
It is because my observation is after the center changes…
I really like your examples, hope you could write more 🙂
Very nice!
Why use centerVariable.asObservable().map and not centerVariable.asObservable().onNext ?
centerVariable.asObservable().subscribe(onNext: { center in
print(“subscribe: (center!)”)
guard let center = center else { return }
let red: CGFloat = ((center.x + center.y).truncatingRemainder(dividingBy: 255.0)) / 255.0 // We just manipulate red, but you can do w/e
let green: CGFloat = 0.0
let blue: CGFloat = 0.0
let backgroundColor = UIColor(red: red, green: green, blue: blue, alpha: 1)
UIView.animate(withDuration: 0.1, animations: {
self.circleView.backgroundColor = backgroundColor
})
}).addDisposableTo(dispose)
i’ try make it with subcribe and it good activity. I think they are just one solution.
Code under this comment
Very good analogies, thank you.