RxAnimated - animated bindings

RxAnimated — is a library for RxSwift/RxCocoa released in 2017, but can still be helpful for animation implementations. It provides the animated interface for bindings in RxCocoa. The library is supplied with several predefined animations for binding and offers a flexible mechanism for adding custom animations and using them during binding in RxCocoa.

This article is intended for those who want to start working with RxAnimated. To cut a long story short, the purpose of the library is to modify the binding code which exists to some extent so that it adds animation to the user interface instead of making immediate changes.

Opportunities:

  • Adding simple built-in transitions to bindings, such as flip, fade, and tick;
  • Adding custom animations in the block for transitions;
  • Extending RxAnimated to support new binding recipients in your own classes;
  • Extending RxAnimated to support new custom animations.

Start Of Work

To start working with RxAnimated, just add the following line to your Podfile:

pod "RxAnimated"

I would also add that RxAnimated hinges on RxSwift 6.Those who are not familiar with CocoaPods, should better follow the link and get acquainted with it.

Non-animated Binding Code

During binding values to RxCocoa write something like this:

import RxCocoa
...
imageObservable
  .bind(to: imageView.rx.image)

As a result, when a new value in imageObservable is being received, the UIImageView is updated. It will happen all at once, without any animated transition and the users will see something like this on the screen:

Animated Binding Code

With RxAnimated animation can be added to binding in a very simple way, for example:

import RxCocoa
import RxAnimated
...
imageObservable
  .bind(animated: imageView.rx.animated.fade(duration:0.33).image)

With the alteration, the binding starts a transition every time, when it’s necessary to produce side effects, in this case, it’s changing of the image in the UIImageView . The result can be seen on the screen:

The only difference is that bind (animated :) is used instead of bind (to :) and animated.fade (duration: 0.33) (or one of the other standard or custom animations) is inserted between RX and the property you want to use, for instance, UIImage, as in the case in point above.

All the built-in animations work on any UIView element as well as on certain properties, such as UILabel.rx.text or UIImageView.rx.image.

Animations List

Below you can find a list of all the standard properties to bind the animations to and the list of animations themselves.

List of properties for animation bindings:

  1. UIView.rx.animated...isHidden
  2. UIView.rx.animated...alpha
  3. UILabel.rx.animated...text
  4. UILabel.rx.animated...attributedText
  5. UIControl.rx.animated...isEnabled
  6. UIControl.rx.animated...isSelected
  7. UIButton.rx.animated...title
  8. UIButton.rx.animated...image
  9. UIButton.rx.animated...backgroundImage
  10. UIImageView.rx.animated...image
  11. NSLayoutConstraint.rx.animated...constant
  12. NSLayoutConstraint.rx.animated...isActive

List Of The Built-in Animations:

  1. UIView.rx.animated.fade(duration: TimeInterval)
  2. UIView.rx.animated.flip(FlipDirection, duration: TimeInterval)
  3. UIView.rx.animated.tick(FlipDirection, duration: TimeInterval)
  4. UIView.rx.animated.animation(duration: TimeInterval, animations: ()->Void)
  5. NSLayoutConstraint.rx.animated.layout(duration: TimeInterval)

Examples

Let’s first create Observables, which will send us values in a certain period of time and we will show animations in response to them.

 private let timer = Observable<Int>.timer(RxTimeInterval.seconds(0), period: RxTimeInterval.seconds(1), scheduler: MainScheduler.instance)
 
    private var isActiveTimer: Observable<Bool> {return timer
            .scan(true){ acc, _ inreturn!acc
            }}
    private var imageTimer: Observable<UIImage?> {return timer
            .scan("face1"){ _, count inreturn count %2==0 ? "face1":"face2"}
            .map { name inreturn UIImage(named: name)}}
    private var constraintTimer: Observable<CGFloat> {return timer
            .scan(0){ acc, _ inreturn acc ==0 ? 100:0}}
    private var timerHidden: Observable<Bool> {return timer
            .scan(false){ _, count inreturn count %2==0 ? true:false}}
    private var timerAlpha: Observable<CGFloat> {return timer
            .scan(1){ acc, _ inreturn acc > 2 ? 1: acc +1}
            .map { CGFloat(1.0/ $0 )}}

And now let’s see how different animations for different app properties look like. UILabel.text + fade:

   timer
            .map {"Text + fade [\($0)]"}
            .bind(animated: fadeLabel.rx.animated.fade(duration:0.33).text)
            .disposed(by: disposeBag)

UILabel.text + flip:

UILabel.text + tick:

UIImageView.image + fade, UIImageView.isHidden + fade, UIImageView.alpha + fade:

 imageTimer
            .bind(to: fadeImageView.rx.image)
            .disposed(by: disposeBag)
        timerHidden
            .bind(animated: fadeHiddenImageView.rx.animated.fade(duration:1.0).isHidden)
            .disposed(by: disposeBag)
        timerAlpha
            .bind(animated: fadeAlphaImageView.rx.animated.fade(duration:1.0).alpha)
            .disposed(by: disposeBag)
 

UIImageView.image + flip, UIImageView.isHidden + flip, UIImageView.alpha + flip:

UIImageView.image + tick, UIImageView.isHidden + tick, UIImageView.alpha + tick:

UIImageView.image + block, UIImageView.isHidden + block, UIImageView.alpha + block:

    imageTimer
            .bind(animated: blockImageView.rx.animated.animation(duration:0.5, animations:{[weak self]in
                self?.angle +=0.2
                self?.blockImageView.transform = CGAffineTransform(rotationAngle: self?.angle ?? 0)}).image )
            .disposed(by: disposeBag)
        timerHidden
            .bind(animated: blockHiddenImageView.rx.animated.animation(duration:0.5, animations:{[weak self]in
                self?.angle +=0.2
                self?.blockHiddenImageView.transform = CGAffineTransform(rotationAngle: self?.angle ?? 0)}).isHidden )
            .disposed(by: disposeBag)
      timerAlpha
            .bind(animated: blockAlphaImageView.rx.animated.animation(duration:0.5, animations:{[weak self]in
                self?.angle +=0.2
                self?.blockAlphaImageView.transform = CGAffineTransform(rotationAngle: self?.angle ?? 0)}).alpha )
            .disposed(by: disposeBag)

NSLayoutConstraint.constant:

   constraintTimer
            .bind(animated: leadingChangeConstraint.rx.animated.layout(duration:0.33).constant )
            .disposed(by: disposeBag)

NSLayoutConstraint.isActive:

    isActiveTimer
            .bind(animated: leadingEnabledDisabledConstraint.rx.animated.layout(duration:0.33).isActive )
            .disposed(by: disposeBag)

UIButton.backgroundImage + fade:

   imageTimer
            .bind(animated: fadeButton.rx.animated.fade(duration:1.0).backgroundImage)
            .disposed(by: disposeBag)
        isActiveTimer
            .bind(to: fadeButton.rx.isEnabled)
            .disposed(by: disposeBag)

UIButton.backgroundImage + flip:

UIButton.backgroundImage + tick:

Adding Custom Animation

As it was mentioned earlier, custom animation can be added to the bindings to match the visual style of any application.

If we need an animation for a property that does not belong to the standard ones, font UILabel, for example, we need to add an extension for this property.

// This is your class `UILabel`
extension AnimatedSink where Base: UILabel {// This is your property name `font` and value type `UIFont`
    public var font: Binder<UIFont> {
        let animation = self.type!return Binder(self.base){ label, font in
            animation.animate(view: label, binding:{
                label.font = font
            })}}}

Then we add a method for our animation:

// This is your class `UIView`
extension AnimatedSink where Base: UIView {// This is your animation name `scale`
    public func scale(_ scale: CGFloat =2, duration: TimeInterval)-> AnimatedSink<Base> {// use one of the animation types and provide `setup` and `animation` blocks
        let type = AnimationType<Base>(type: RxAnimationType.spring(damping:1, velocity:0), duration: duration, setup:{ view in
            view.alpha =0
            view.transform = CGAffineTransform(scaleX: scale, y: scale)}, animations:{ view in
            view.alpha =1
            view.transform = CGAffineTransform.identity
        })
 
        //return AnimatedSinkreturn AnimatedSink<Base>(base: self.base, type: type)}}

And now we can use this animation in our bindings. Let's add our animation to UILabel, UIImageView, UIButton and see what happened:

UILabel.text + custom:

 timer
            .map {"Text + custom [\($0)]"}
            .bind(animated: customLabel.rx.animated.scale(duration:0.33).text)
            .disposed(by: disposeBag)

UIImageView.image + custom, UIImageView.isHidden + custom, UIImageView.alpha + custom:

        imageTimer
            .bind(animated: customImageView.rx.animated.scale(duration:1.0).image)
            .disposed(by: disposeBag)
 

UIButton.backgroundImage + custom:

 imageTimer
            .bind(animated: customButton.rx.animated.scale(duration:1.0).backgroundImage)
            .disposed(by: disposeBag)
        isActiveTimer
            .bind(to: customButton.rx.isEnabled)
            .disposed(by: disposeBag)

Conclusion

Congratulations, we’ve made a small app with all the RxAnimated built-in standard animations and added custom animations as well. So you can now add animations to bindings and create the animations of your own through RxAnimated library extension.

You’ve also got acquainted with the lists of standard animations and object properties for animations. RxAnimated — is quite a convenient library for adding animations to the reactive code. I hope the article was of use and wish you good luck in your undertakings.

The full demo project can be found on Bitbucket