From a0831fe68cc6e5a33b7ff8479a72ac9fd8277fb6 Mon Sep 17 00:00:00 2001 From: Marin Todorov Date: Fri, 22 Sep 2017 15:53:51 +0200 Subject: [PATCH] fix for auto layout animations + readme --- Example/RxAnimated/Base.lproj/Main.storyboard | 77 ++++++++++++++----- Example/RxAnimated/Info.plist | 1 - Example/RxAnimated/ViewController.swift | 26 ++++++- README.md | 13 +++- RxAnimated/Core/RxAnimated+animations.swift | 30 +++++--- RxAnimated/Core/RxAnimated+bindings.swift | 45 +++++++---- RxAnimated/Core/RxAnimated.swift | 15 +--- 7 files changed, 143 insertions(+), 64 deletions(-) diff --git a/Example/RxAnimated/Base.lproj/Main.storyboard b/Example/RxAnimated/Base.lproj/Main.storyboard index bac753c..66f4065 100644 --- a/Example/RxAnimated/Base.lproj/Main.storyboard +++ b/Example/RxAnimated/Base.lproj/Main.storyboard @@ -7,6 +7,7 @@ + @@ -22,26 +23,28 @@ - + + - + + + + + + - + + - - + - - + @@ -159,6 +194,8 @@ + + diff --git a/Example/RxAnimated/Info.plist b/Example/RxAnimated/Info.plist index eb18faa..6c48029 100644 --- a/Example/RxAnimated/Info.plist +++ b/Example/RxAnimated/Info.plist @@ -33,7 +33,6 @@ UISupportedInterfaceOrientations UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft diff --git a/Example/RxAnimated/ViewController.swift b/Example/RxAnimated/ViewController.swift index e9e0ef6..05850ee 100644 --- a/Example/RxAnimated/ViewController.swift +++ b/Example/RxAnimated/ViewController.swift @@ -26,6 +26,9 @@ class ViewController: UIViewController { @IBOutlet var labelIsHidden: UILabel! @IBOutlet var imageIsHidden: UIImageView! + @IBOutlet var leftConstraint: NSLayoutConstraint! + @IBOutlet var rightConstraint: NSLayoutConstraint! + private let timer = Observable.timer(0, period: 1, scheduler: MainScheduler.instance).shareReplay(1) private let bag = DisposeBag() @@ -34,21 +37,21 @@ class ViewController: UIViewController { // Animate `text` with a crossfade timer - .map { "label fade [\($0)]" } + .map { "Text + fade [\($0)]" } .bind(animated: labelFade.rx.animated.fade(duration: 0.33).text) .disposed(by: bag) // Animate `text` with a top flip timer .delay(0.33, scheduler: MainScheduler.instance) - .map { "label flip top [\($0)]" } + .map { "Text + flip [\($0)]" } .bind(animated: labelFlip.rx.animated.flip(.top, duration: 0.33).text) .disposed(by: bag) // Animate `text` with a custom animation `tick`, as driver timer .delay(0.67, scheduler: MainScheduler.instance) - .map { "custom tick animation [\($0)]" } + .map { "Text + custom [\($0)]" } .asDriver(onErrorJustReturn: "error") .bind(animated: labelCustom.rx.animated.tick(.left, duration: 0.75).text) .disposed(by: bag) @@ -64,6 +67,22 @@ class ViewController: UIViewController { .bind(animated: imageFlip.rx.animated.tick(.right, duration: 1.0).image) .disposed(by: bag) + // Animate layout constraint + timer + .scan(0) { acc, _ in + return acc == 0 ? 105 : 0 + } + .bind(animated: leftConstraint.rx.animated.layout(duration: 0.33).constant ) + .disposed(by: bag) + + // Activate/Deactivate a constraint + timer + .scan(true) { acc, _ in + return !acc + } + .bind(animated: rightConstraint.rx.animated.layout(duration: 0.33).isActive ) + .disposed(by: bag) + // Animate `alpha` with a flip let timerAlpha = timer .scan(1) { acc, _ in @@ -92,6 +111,7 @@ class ViewController: UIViewController { .map { "hidden: \($0)" } .bind(to: labelIsHidden.rx.text) .disposed(by: bag) + } } diff --git a/README.md b/README.md index 604d6b7..01278a5 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![License](https://img.shields.io/cocoapods/l/RxAnimated.svg?style=flat)](http://cocoapods.org/pods/RxAnimated) [![Platform](https://img.shields.io/cocoapods/p/RxAnimated.svg?style=flat)](http://cocoapods.org/pods/RxAnimated) -> **NB:** This is a pre-release software, currently the lib works with Xcode9 GM and RxSwift4 beta.0 - and Xcode doesn't offer autocomplete when you use it. +> **NB:** This is a pre-release software, currently the lib works with Xcode9 GM and RxSwift4 beta.0. **RxAnimated** provides animation interface to RxCocoa's bindings. @@ -46,7 +46,15 @@ List of built-in animated sinks: UIView.rx.animated...isHidden UIView.rx.animated...alpha UILabel.rx.animated...text +UILabel.rx.animated...attributedText +UIControl.rx.animated...isEnabled +UIControl.rx.animated...isSelected +UIButton.rx.animated...title +UIButton.rx.animated...image +UIButton.rx.animated...backgroundImage UIImageView.rx.animated...image +NSLayoutConstraint.rx.animated...constant +NSLayoutConstraint.rx.animated...isActive ``` List of the built-in animations: @@ -55,8 +63,11 @@ List of the built-in animations: UIView.rx.animated.fade(duration: TimeInterval) UIView.rx.animated.flip(FlipDirection, duration: TimeInterval) UIView.rx.animated.tick(FlipDirection, duration: TimeInterval) +NSLayoutConstraint.rx.animated.layout(duration: TimeInterval) ``` +Check the demo app for a number of examples. + ## Custom animations You can easily add your custom bind animations to match the visual style of your app. diff --git a/RxAnimated/Core/RxAnimated+animations.swift b/RxAnimated/Core/RxAnimated+animations.swift index f8b8157..de4eaa4 100644 --- a/RxAnimated/Core/RxAnimated+animations.swift +++ b/RxAnimated/Core/RxAnimated+animations.swift @@ -5,16 +5,6 @@ import RxSwift import RxCocoa -// MARK: - built-in animations - -extension AnimatedSink where Base: UIView { - /// cross-dissolve animation on `UIView` - public func fade(duration: TimeInterval) -> AnimatedSink { - let type = AnimationType(type: RxAnimationType.transition(.transitionCrossDissolve), duration: duration, animations: nil) - return AnimatedSink(base: self.base, type: type) - } -} - /// custom direction enumeration public enum FlipDirection { case left, right, top, bottom @@ -29,15 +19,21 @@ public enum FlipDirection { } } +// MARK: - built-in animations + extension AnimatedSink where Base: UIView { + /// cross-dissolve animation on `UIView` + public func fade(duration: TimeInterval) -> AnimatedSink { + let type = AnimationType(type: RxAnimationType.transition(.transitionCrossDissolve), duration: duration, animations: nil) + return AnimatedSink(base: self.base, type: type) + } + /// flip animation on `UIView` public func flip(_ direction: FlipDirection, duration: TimeInterval) -> AnimatedSink { let type = AnimationType(type: RxAnimationType.transition(direction.viewTransition), duration: duration, animations: nil) return AnimatedSink(base: self.base, type: type) } -} -extension AnimatedSink where Base: UIView { /// example of extending RxAnimated with a custom animation public func tick(_ direction: FlipDirection = .right, duration: TimeInterval) -> AnimatedSink { let type = AnimationType(type: RxAnimationType.spring(damping: 0.33, velocity: 0), duration: duration, setup: { view in @@ -50,3 +46,13 @@ extension AnimatedSink where Base: UIView { return AnimatedSink(base: self.base, type: type) } } + +extension AnimatedSink where Base: NSLayoutConstraint { + /// auto layout animations + public func layout(duration: TimeInterval) -> AnimatedSink { + let type = AnimationType(type: RxAnimationType.animation, duration: duration, animations: { view in + view.layoutIfNeeded() + }) + return AnimatedSink(base: self.base, type: type) + } +} diff --git a/RxAnimated/Core/RxAnimated+bindings.swift b/RxAnimated/Core/RxAnimated+bindings.swift index f25b12f..ef40394 100644 --- a/RxAnimated/Core/RxAnimated+bindings.swift +++ b/RxAnimated/Core/RxAnimated+bindings.swift @@ -5,11 +5,20 @@ import RxSwift import RxCocoa +// MARK: - Reactive ext on UIView + +extension Reactive where Base: UIView { + /// adds animated bindings to view classes under `rx.animated` + public var animated: AnimatedSink { + return AnimatedSink(base: self.base) + } +} + // MARK: - UIView extension AnimatedSink where Base: UIView { public var isHidden: Binder { return Binder(self.base) { view, hidden in - self.type.animate(view: view, block: { + self.type.animate(view: view, binding: { view.isHidden = hidden }) } @@ -19,7 +28,7 @@ extension AnimatedSink where Base: UIView { extension AnimatedSink where Base: UIView { public var alpha: Binder { return Binder(self.base) { view, alpha in - self.type.animate(view: view, block: { + self.type.animate(view: view, binding: { view.alpha = alpha }) } @@ -30,7 +39,7 @@ extension AnimatedSink where Base: UIView { extension AnimatedSink where Base: UILabel { public var text: Binder { return Binder(self.base) { label, text in - self.type.animate(view: label, block: { + self.type.animate(view: label, binding: { guard let label = label as? UILabel else { return } label.text = text }) @@ -38,7 +47,7 @@ extension AnimatedSink where Base: UILabel { } public var attributedText: Binder { return Binder(self.base) { label, text in - self.type.animate(view: label, block: { + self.type.animate(view: label, binding: { guard let label = label as? UILabel else { return } label.attributedText = text }) @@ -50,7 +59,7 @@ extension AnimatedSink where Base: UILabel { extension AnimatedSink where Base: UIControl { public var isEnabled: Binder { return Binder(self.base) { control, enabled in - self.type.animate(view: control, block: { + self.type.animate(view: control, binding: { guard let control = control as? UIControl else { return } control.isEnabled = enabled }) @@ -58,7 +67,7 @@ extension AnimatedSink where Base: UIControl { } public var isSelected: Binder { return Binder(self.base) { control, selected in - self.type.animate(view: control, block: { + self.type.animate(view: control, binding: { guard let control = control as? UIControl else { return } control.isSelected = selected }) @@ -70,7 +79,7 @@ extension AnimatedSink where Base: UIControl { extension AnimatedSink where Base: UIButton { public var title: Binder { return Binder(self.base) { button, title in - self.type.animate(view: button, block: { + self.type.animate(view: button, binding: { guard let button = button as? UIButton else { return } button.setTitle(title, for: button.state) }) @@ -78,7 +87,7 @@ extension AnimatedSink where Base: UIButton { } public var image: Binder { return Binder(self.base) { button, image in - self.type.animate(view: button, block: { + self.type.animate(view: button, binding: { guard let button = button as? UIButton else { return } button.setImage(image, for: button.state) }) @@ -86,7 +95,7 @@ extension AnimatedSink where Base: UIButton { } public var backgroundImage: Binder { return Binder(self.base) { button, image in - self.type.animate(view: button, block: { + self.type.animate(view: button, binding: { guard let button = button as? UIButton else { return } button.setBackgroundImage(image, for: button.state) }) @@ -98,7 +107,7 @@ extension AnimatedSink where Base: UIButton { extension AnimatedSink where Base: UIImageView { public var image: Binder { return Binder(self.base) { imageView, image in - self.type.animate(view: imageView, block: { + self.type.animate(view: imageView, binding: { guard let imageView = imageView as? UIImageView else { return } imageView.image = image }) @@ -106,17 +115,22 @@ extension AnimatedSink where Base: UIImageView { } } -// MARK: - NSLayoutConstraint - -// TODO: test this in the demo app +// MARK: - Reactive ext on NSLayoutConstraint +extension Reactive where Base: NSLayoutConstraint { + /// adds animated bindings to view classes under `rx.animated` + public var animated: AnimatedSink { + return AnimatedSink(base: self.base) + } +} +// MARK: - NSLayoutConstraint extension AnimatedSink where Base: NSLayoutConstraint { public var constant: Binder { return Binder(self.base) { constraint, constant in guard let view = constraint.firstItem as? UIView, let superview = view.superview else { return } - self.type.animate(view: superview, block: { + self.type.animate(view: superview, binding: { constraint.constant = constant }) } @@ -126,10 +140,9 @@ extension AnimatedSink where Base: NSLayoutConstraint { guard let view = constraint.firstItem as? UIView, let superview = view.superview else { return } - self.type.animate(view: superview, block: { + self.type.animate(view: superview, binding: { constraint.isActive = active }) } } } - diff --git a/RxAnimated/Core/RxAnimated.swift b/RxAnimated/Core/RxAnimated.swift index 1bf2a33..039a885 100644 --- a/RxAnimated/Core/RxAnimated.swift +++ b/RxAnimated/Core/RxAnimated.swift @@ -54,25 +54,25 @@ public struct AnimationType { * - parameter view: a view to run the animations on * - parameter block: a custom block to inject inside the animation */ - func animate(view: UIView, block: (()->Void)?) { + func animate(view: UIView, binding: (()->Void)?) { setup?(view) DispatchQueue.main.async { switch self.type { case .animation: UIView.animate(withDuration: self.duration, delay: 0, options: self.options, animations: { + binding?() self.animations?(view) - block?() }, completion: self.completion) case .transition(let type): UIView.transition(with: view, duration: self.duration, options: self.options.union(type), animations: { + binding?() self.animations?(view) - block?() }, completion: self.completion) case .spring(let damping, let velocity): UIView.animate(withDuration: self.duration, delay: 0, usingSpringWithDamping: damping, initialSpringVelocity: velocity, options: self.options, animations: { + binding?() self.animations?(view) - block?() }, completion: self.completion) } } @@ -95,13 +95,6 @@ extension SharedSequence { } } -extension Reactive where Base: UIView { - /// adds animated bindings to view classes under `rx.animated` - public var animated: AnimatedSink { - return AnimatedSink(base: self.base) - } -} - /// an animations proxy data type public struct AnimatedSink { internal var type: AnimationType!