RxAnimated - animated bindings

RxAnimated — is a library for RxSwift / RxCocoa which was released not long ago. 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 5.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, _ in
                return !acc
            }
    }
    private var imageTimer: Observable<UIImage?> {
        return timer
            .scan("face1") { _, count in
                return count % 2 == 0 ? "face1" : "face2"
            }
            .map { name in
                return UIImage(named: name)
            }
    }
    private var constraintTimer: Observable<CGFloat> {
        return timer
            .scan(0) { acc, _ in
                return acc == 0 ? 100 : 0
        }
    }
    private var timerHidden: Observable<Bool> {
        return timer
            .scan(false) { _, count in
                return count % 2 == 0 ? true : false
            }
    }
    private var timerAlpha: Observable<CGFloat> {
        return timer
            .scan(1) { acc, _ in
                return 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 AnimatedSink
        return 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