From 2d15e9e4081d4261004f82459e851c531a629a45 Mon Sep 17 00:00:00 2001 From: Thibault Wittemberg Date: Thu, 29 Mar 2018 22:07:54 -0400 Subject: [PATCH 1/8] Add UIViewPropertyAnimator reactive extension This commit adds a Rx extension to UIViewPropertyAnimator. The purpose is to trigger a Completable(.completed) once the animation is ended so it is easy to chain them with the syntactic sugar 'andThen'. --- CHANGELOG.md | 4 + .../Index.xcplaygroundpage/Contents.swift | 1 + .../Contents.swift | 101 ++++++++++++++++++ .../timeline.xctimeline | 6 ++ .../contents.xcplayground | 1 + RxSwiftExt.xcodeproj/project.pbxproj | 16 +++ .../RxCocoa/UIViewPropertyAnimator+Rx.swift | 38 +++++++ .../UIViewPropertyAnimatorTests+Rx.swift | 40 +++++++ 8 files changed, 207 insertions(+) create mode 100644 Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/Contents.swift create mode 100644 Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/timeline.xctimeline create mode 100644 Source/RxCocoa/UIViewPropertyAnimator+Rx.swift create mode 100644 Tests/RxCocoa/UIViewPropertyAnimatorTests+Rx.swift diff --git a/CHANGELOG.md b/CHANGELOG.md index f63fd110..b6f8c898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ master ----- - added `withUnretained(_:)` operator +3.3.0 +----- +- added `animate(afterDelay:)` operator UIViewPropertyAnimator + 3.2.0 ----- - added `mapAt(keyPath:)` operator diff --git a/Playground/RxSwiftExtPlayground.playground/Pages/Index.xcplaygroundpage/Contents.swift b/Playground/RxSwiftExtPlayground.playground/Pages/Index.xcplaygroundpage/Contents.swift index 679b9641..7083fe9e 100644 --- a/Playground/RxSwiftExtPlayground.playground/Pages/Index.xcplaygroundpage/Contents.swift +++ b/Playground/RxSwiftExtPlayground.playground/Pages/Index.xcplaygroundpage/Contents.swift @@ -41,6 +41,7 @@ - [filterMap()](filterMap) operator, filters out some values and maps the rest (replaces `filter` + `map` combo) - [Observable.fromAsync()](fromAsync) constructor, translates an async function that returns data through a completionHandler in a function that returns data through an Observable - [ofType()](ofType) operator, filters the elements of an observable sequence, if that is an instance of the supplied type. + - [UIViewPropertyAnimator.rx.animate](animate) operator, returns a Completable that completes as soon as the animation ends. */ //: [Next >>](@next) diff --git a/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/Contents.swift b/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/Contents.swift new file mode 100644 index 00000000..1257206b --- /dev/null +++ b/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/Contents.swift @@ -0,0 +1,101 @@ +/*: + > # IMPORTANT: To use `RxSwiftExtPlayground.playground`, please: + + 1. Make sure you have [Carthage](https://github.com/Carthage/Carthage) installed + 1. Fetch Carthage dependencies from shell: `carthage bootstrap --platform ios` + 1. Build scheme `RxSwiftExtPlayground` scheme for a simulator target + 1. Choose `View > Show Debug Area` + */ + +//: [Previous](@previous) + +import RxSwift +import RxCocoa +import RxSwiftExt +import PlaygroundSupport +import UIKit + +/*: + ## animate + + The `animate` operators provide a Completable that when subscribed to starts the animation (after a delay or not) and completes once the animation is ended + Please, active the assistant editor to see the preview + */ + +class AnimateViewController : UIViewController { + + let disposeBag = DisposeBag() + + lazy var box1: UIView = { + let view = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100)) + view.backgroundColor = .red + return view + }() + + lazy var box2: UIView = { + let view = UIView(frame: CGRect(x: 100, y: 220, width: 100, height: 100)) + view.backgroundColor = .green + return view + }() + + lazy var box3: UIView = { + let view = UIView(frame: CGRect(x: 100, y: 340, width: 100, height: 100)) + view.backgroundColor = .blue + return view + }() + + lazy var button: UIButton = { + let button = UIButton(frame: CGRect(x: 100, y: 500, width: 200, height: 50)) + button.setTitle("Play animation", for: .normal) + button.setTitleColor(.blue, for: .normal) + button.backgroundColor = .white + button.rx.tap.subscribe(onNext: { [unowned self] (_) in + self.animate() + }).disposed(by: self.disposeBag) + return button + }() + + lazy var animator1 = { + UIViewPropertyAnimator(duration: 0.5, curve: .linear) { [unowned self] in + self.box1.transform = CGAffineTransform(translationX: 0, y: -100) + } + }() + + lazy var animator2 = { + UIViewPropertyAnimator(duration: 0.3, curve: .linear) { [unowned self] in + self.box2.transform = CGAffineTransform(translationX: 0, y: -100).scaledBy(x: 1.2, y: 1.2) + } + }() + + lazy var animator3 = { + UIViewPropertyAnimator(duration: 0.2, curve: .linear) { [unowned self] in + self.box3.transform = CGAffineTransform(translationX: 0, y: -100).rotated(by: CGFloat(M_PI)) + } + }() + + private func animate () { + // trigger the animation chain + self.animator1.rx.animate() + .andThen(self.animator2.rx.animate(afterDelay: 0.2)) + .andThen(self.animator3.rx.animate(afterDelay: 0.1)) + .subscribe() + .disposed(by: self.disposeBag) + } + + override func viewDidLoad() { + super.viewDidLoad() + + // construct the main view + self.view.backgroundColor = .white + self.view.addSubview(self.box1) + self.view.addSubview(self.box2) + self.view.addSubview(self.box3) + self.view.addSubview(self.button) + } + +} + +// Present the view controller in the Live View window +PlaygroundPage.current.liveView = AnimateViewController() + +//: [Next](@next) diff --git a/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/timeline.xctimeline b/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/timeline.xctimeline new file mode 100644 index 00000000..bf468afe --- /dev/null +++ b/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/timeline.xctimeline @@ -0,0 +1,6 @@ + + + + + diff --git a/Playground/RxSwiftExtPlayground.playground/contents.xcplayground b/Playground/RxSwiftExtPlayground.playground/contents.xcplayground index 71ec5607..a81a2373 100644 --- a/Playground/RxSwiftExtPlayground.playground/contents.xcplayground +++ b/Playground/RxSwiftExtPlayground.playground/contents.xcplayground @@ -23,5 +23,6 @@ + \ No newline at end of file diff --git a/RxSwiftExt.xcodeproj/project.pbxproj b/RxSwiftExt.xcodeproj/project.pbxproj index d5a539c1..e28c760a 100644 --- a/RxSwiftExt.xcodeproj/project.pbxproj +++ b/RxSwiftExt.xcodeproj/project.pbxproj @@ -23,6 +23,10 @@ /* Begin PBXBuildFile section */ 188C6DA31C47B4240092101A /* RxSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 188C6DA21C47B4240092101A /* RxSwift.framework */; }; + 1A8741AC20745A91004BB762 /* UIViewPropertyAnimatorTests+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A8741AB20745A91004BB762 /* UIViewPropertyAnimatorTests+Rx.swift */; }; + 1A8741AD20745A96004BB762 /* UIViewPropertyAnimatorTests+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A8741AB20745A91004BB762 /* UIViewPropertyAnimatorTests+Rx.swift */; }; + 1A8741AE20745A97004BB762 /* UIViewPropertyAnimatorTests+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A8741AB20745A91004BB762 /* UIViewPropertyAnimatorTests+Rx.swift */; }; + 1AA8395B207451D6001C49ED /* RxCocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1AA8395A207451D5001C49ED /* RxCocoa.framework */; }; 3D11958B1FCAD9AE0095134B /* and.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBDE5FB1FBBAE3900DF47F9 /* and.swift */; }; 3D11958C1FCAD9AF0095134B /* and.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBDE5FB1FBBAE3900DF47F9 /* and.swift */; }; 3D638DEC1DC2B2D50089A590 /* RxSwiftExt.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 188C6D911C47B2B20092101A /* RxSwiftExt.framework */; }; @@ -32,6 +36,7 @@ 3DBDE5FF1FBBB09900DF47F9 /* AndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBDE5FD1FBBB05400DF47F9 /* AndTests.swift */; }; 3DBDE6001FBBB09A00DF47F9 /* AndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBDE5FD1FBBB05400DF47F9 /* AndTests.swift */; }; 3DBDE6011FBBB09A00DF47F9 /* AndTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3DBDE5FD1FBBB05400DF47F9 /* AndTests.swift */; }; + 4A73956C206D501300E2BE2D /* UIViewPropertyAnimator+Rx.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A73956B206D501300E2BE2D /* UIViewPropertyAnimator+Rx.swift */; }; 5386076F1E6F1C0A000361DE /* mapTo+RxCocoa.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5386076E1E6F1C0A000361DE /* mapTo+RxCocoa.swift */; }; 538607731E6F1D51000361DE /* MapToTests+RxCocoa.swift in Sources */ = {isa = PBXBuildFile; fileRef = 538607711E6F1CFB000361DE /* MapToTests+RxCocoa.swift */; }; 538607AA1E6F334B000361DE /* apply.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5386079A1E6F334B000361DE /* apply.swift */; }; @@ -247,11 +252,14 @@ /* Begin PBXFileReference section */ 188C6D911C47B2B20092101A /* RxSwiftExt.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RxSwiftExt.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 188C6DA21C47B4240092101A /* RxSwift.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxSwift.framework; path = Carthage/Build/iOS/RxSwift.framework; sourceTree = SOURCE_ROOT; }; + 1A8741AB20745A91004BB762 /* UIViewPropertyAnimatorTests+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewPropertyAnimatorTests+Rx.swift"; sourceTree = ""; }; + 1AA8395A207451D5001C49ED /* RxCocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxCocoa.framework; path = Carthage/Build/iOS/RxCocoa.framework; sourceTree = ""; }; 3D638DE71DC2B2D40089A590 /* RxSwiftExtTests-iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "RxSwiftExtTests-iOS.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 3D638E1E1DC2B3A40089A590 /* RxTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = RxTest.framework; path = Carthage/Build/iOS/RxTest.framework; sourceTree = ""; }; 3DB034F61DC376D9002C6A26 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Tests/Info.plist; sourceTree = ""; }; 3DBDE5FB1FBBAE3900DF47F9 /* and.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = and.swift; sourceTree = ""; }; 3DBDE5FD1FBBB05400DF47F9 /* AndTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AndTests.swift; sourceTree = ""; }; + 4A73956B206D501300E2BE2D /* UIViewPropertyAnimator+Rx.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewPropertyAnimator+Rx.swift"; sourceTree = ""; }; 5386076E1E6F1C0A000361DE /* mapTo+RxCocoa.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "mapTo+RxCocoa.swift"; path = "Source/RxCocoa/mapTo+RxCocoa.swift"; sourceTree = SOURCE_ROOT; }; 538607711E6F1CFB000361DE /* MapToTests+RxCocoa.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MapToTests+RxCocoa.swift"; sourceTree = ""; }; 5386079A1E6F334B000361DE /* apply.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = apply.swift; path = Source/RxSwift/apply.swift; sourceTree = SOURCE_ROOT; }; @@ -325,6 +333,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 1AA8395B207451D6001C49ED /* RxCocoa.framework in Frameworks */, 188C6DA31C47B4240092101A /* RxSwift.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -429,6 +438,7 @@ 53F336E71E70CBF700D35D38 /* distinct+RxCocoa.swift */, 5386076E1E6F1C0A000361DE /* mapTo+RxCocoa.swift */, 538607EF1E6F589E000361DE /* not+RxCocoa.swift */, + 4A73956B206D501300E2BE2D /* UIViewPropertyAnimator+Rx.swift */, ); path = RxCocoa; sourceTree = ""; @@ -439,6 +449,7 @@ 53F336E91E70D59000D35D38 /* DistinctTests+RxCocoa.swift */, 538607711E6F1CFB000361DE /* MapToTests+RxCocoa.swift */, 53C79D5F1E6F5AAB00CD9B6A /* NotTests+RxCocoa.swift */, + 1A8741AB20745A91004BB762 /* UIViewPropertyAnimatorTests+Rx.swift */, ); name = RxCocoa; path = Tests/RxCocoa; @@ -532,6 +543,7 @@ 9DAB77991D6763AC007E85BC /* Frameworks */ = { isa = PBXGroup; children = ( + 1AA8395A207451D5001C49ED /* RxCocoa.framework */, E36BDFB81F38755F008C9D56 /* tvOS */, 62512C561F0EAEB90083A89F /* macOS */, 62512C551F0EAEB20083A89F /* iOS */, @@ -933,6 +945,7 @@ 538607B81E6F334B000361DE /* retryWithBehavior.swift in Sources */, 66C663061EA0ECD9005245C4 /* materialized+elements.swift in Sources */, 538607B51E6F334B000361DE /* once.swift in Sources */, + 4A73956C206D501300E2BE2D /* UIViewPropertyAnimator+Rx.swift in Sources */, 538607B11E6F334B000361DE /* mapTo.swift in Sources */, 538607AA1E6F334B000361DE /* apply.swift in Sources */, C4D2153F20118A81009804AE /* ofType.swift in Sources */, @@ -989,6 +1002,7 @@ 538607731E6F1D51000361DE /* MapToTests+RxCocoa.swift in Sources */, 538607E61E6F36A9000361DE /* MapToTests.swift in Sources */, 5A5FCE411ED5AEC60052A9B5 /* PausableBufferedTests.swift in Sources */, + 1A8741AC20745A91004BB762 /* UIViewPropertyAnimatorTests+Rx.swift in Sources */, 3DBDE5FF1FBBB09900DF47F9 /* AndTests.swift in Sources */, 58C545FD1AE234C7F290334F /* ZipWithTest.swift in Sources */, ); @@ -1062,6 +1076,7 @@ 62512CA21F0EB1850083A89F /* WeakTarget.swift in Sources */, 62512C9D1F0EB1850083A89F /* PausableTests.swift in Sources */, 62512C991F0EB1850083A89F /* MapToTests.swift in Sources */, + 1A8741AE20745A97004BB762 /* UIViewPropertyAnimatorTests+Rx.swift in Sources */, 3DBDE6001FBBB09A00DF47F9 /* AndTests.swift in Sources */, 58C54302EC14B6FF2034BAF6 /* ZipWithTest.swift in Sources */, ); @@ -1135,6 +1150,7 @@ E39C42091F18B13E007F2ACD /* NotTests.swift in Sources */, E39C42081F18B13E007F2ACD /* Materialized+elementsTests.swift in Sources */, E39C42071F18B13E007F2ACD /* MapToTests.swift in Sources */, + 1A8741AD20745A96004BB762 /* UIViewPropertyAnimatorTests+Rx.swift in Sources */, 3DBDE6011FBBB09A00DF47F9 /* AndTests.swift in Sources */, 58C54B6E1B4C678DE2378145 /* ZipWithTest.swift in Sources */, ); diff --git a/Source/RxCocoa/UIViewPropertyAnimator+Rx.swift b/Source/RxCocoa/UIViewPropertyAnimator+Rx.swift new file mode 100644 index 00000000..bae0f4f6 --- /dev/null +++ b/Source/RxCocoa/UIViewPropertyAnimator+Rx.swift @@ -0,0 +1,38 @@ +// +// UIViewPropertyAnimator+Rx.swift +// RxSwiftExt +// +// Created by Wittemberg, Thibault on 29/03/18. +// Copyright © 2017 RxSwift Community. All rights reserved. +// + +import Foundation +import UIKit +import RxSwift +import RxCocoa + +@available(iOSApplicationExtension 10.0, *) +extension Reactive where Base == UIViewPropertyAnimator { + + /// Completable that when subscribed to, starts the animation after a delay + /// and completes once the animation is ended + /// + /// - Parameter delay: the delay to apply to the animation start + /// - Returns: the Completable that will send .completed once the animation is ended + public func animate(afterDelay delay: Double = 0) -> Completable { + + return Completable.create { [base] completable in + + base.addCompletion({ (position) in + guard position == .end else { return } + completable(.completed) + }) + + base.startAnimation(afterDelay: delay) + + return Disposables.create { + base.stopAnimation(true) + } + } + } +} diff --git a/Tests/RxCocoa/UIViewPropertyAnimatorTests+Rx.swift b/Tests/RxCocoa/UIViewPropertyAnimatorTests+Rx.swift new file mode 100644 index 00000000..e8c05ef0 --- /dev/null +++ b/Tests/RxCocoa/UIViewPropertyAnimatorTests+Rx.swift @@ -0,0 +1,40 @@ +// +// UIViewPropertyAnimatorTests+Rx.swift +// RxSwiftExt +// +// Created by Thibault Wittemberg on 3/4/18. +// Copyright © 2017 RxSwift Community. All rights reserved. +// + +import XCTest +import RxSwift +import RxCocoa +import RxSwiftExt +import UIKit + +class UIViewPropertyAnimatorTests: XCTestCase { + + let disposeBag = DisposeBag() + + func testAnimationCompleted() { + + let expectations = expectation(description: "Animation completed") + + let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) + if #available(iOS 10.0, *) { + let animator = UIViewPropertyAnimator(duration: 0.5, curve: .linear) { + view.transform = CGAffineTransform(translationX: 100, y: 100) + } + animator.rx.animate().subscribe(onCompleted: { + XCTAssertEqual(100, view.frame.origin.x) + XCTAssertEqual(100, view.frame.origin.y) + expectations.fulfill() + }).disposed(by: self.disposeBag) + } else { + // Fallback on earlier versions + expectations.fulfill() + } + + waitForExpectations(timeout: 1) + } +} From 10d3cd50cf6232c9cc801a2832f39a153e46edd1 Mon Sep 17 00:00:00 2001 From: freak4pc Date: Wed, 4 Apr 2018 13:13:50 +0300 Subject: [PATCH 2/8] Made extension public and cleaned up docs a bit. --- .../RxCocoa/UIViewPropertyAnimator+Rx.swift | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Source/RxCocoa/UIViewPropertyAnimator+Rx.swift b/Source/RxCocoa/UIViewPropertyAnimator+Rx.swift index bae0f4f6..44619146 100644 --- a/Source/RxCocoa/UIViewPropertyAnimator+Rx.swift +++ b/Source/RxCocoa/UIViewPropertyAnimator+Rx.swift @@ -11,22 +11,20 @@ import UIKit import RxSwift import RxCocoa -@available(iOSApplicationExtension 10.0, *) -extension Reactive where Base == UIViewPropertyAnimator { - - /// Completable that when subscribed to, starts the animation after a delay - /// and completes once the animation is ended +@available(iOS 10.0, *) +public extension Reactive where Base: UIViewPropertyAnimator { + /// Provides a Completable that triggers the UIViewPropertyAnimator upon subscription + /// and completes once the animation ends. /// - /// - Parameter delay: the delay to apply to the animation start - /// - Returns: the Completable that will send .completed once the animation is ended - public func animate(afterDelay delay: Double = 0) -> Completable { - + /// - Parameter afterDelay: the delay to apply to the animation start + /// + /// - Returns: Completable + func animate(afterDelay delay: TimeInterval = 0) -> Completable { return Completable.create { [base] completable in - - base.addCompletion({ (position) in + base.addCompletion { position in guard position == .end else { return } completable(.completed) - }) + } base.startAnimation(afterDelay: delay) From 149fc766be29732ad8b3706e9f532af6dbf17c5b Mon Sep 17 00:00:00 2001 From: freak4pc Date: Wed, 4 Apr 2018 13:13:59 +0300 Subject: [PATCH 3/8] Minor updates to Playground page --- .../Index.xcplaygroundpage/Contents.swift | 2 +- .../Contents.swift | 57 ++++++++++--------- 2 files changed, 32 insertions(+), 27 deletions(-) diff --git a/Playground/RxSwiftExtPlayground.playground/Pages/Index.xcplaygroundpage/Contents.swift b/Playground/RxSwiftExtPlayground.playground/Pages/Index.xcplaygroundpage/Contents.swift index 7083fe9e..2644b501 100644 --- a/Playground/RxSwiftExtPlayground.playground/Pages/Index.xcplaygroundpage/Contents.swift +++ b/Playground/RxSwiftExtPlayground.playground/Pages/Index.xcplaygroundpage/Contents.swift @@ -41,7 +41,7 @@ - [filterMap()](filterMap) operator, filters out some values and maps the rest (replaces `filter` + `map` combo) - [Observable.fromAsync()](fromAsync) constructor, translates an async function that returns data through a completionHandler in a function that returns data through an Observable - [ofType()](ofType) operator, filters the elements of an observable sequence, if that is an instance of the supplied type. - - [UIViewPropertyAnimator.rx.animate](animate) operator, returns a Completable that completes as soon as the animation ends. + - **UIViewPropertyAnimator** [animate()](UIViewPropertyAnimator+Rx) operator, returns a Completable that completes as soon as the animation ends. */ //: [Next >>](@next) diff --git a/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/Contents.swift b/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/Contents.swift index 1257206b..35e60b67 100644 --- a/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/Contents.swift +++ b/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/Contents.swift @@ -18,11 +18,12 @@ import UIKit /*: ## animate - The `animate` operators provide a Completable that when subscribed to starts the animation (after a delay or not) and completes once the animation is ended - Please, active the assistant editor to see the preview + The `animate` operator provides a Completable that triggers the animation upon subscription and completes when the animation ends. + + Please open the Assistant Editor (⌘⌥⏎) to see the Interactive Live View example. */ -class AnimateViewController : UIViewController { +class AnimateViewController: UIViewController { let disposeBag = DisposeBag() @@ -49,50 +50,54 @@ class AnimateViewController : UIViewController { button.setTitle("Play animation", for: .normal) button.setTitleColor(.blue, for: .normal) button.backgroundColor = .white - button.rx.tap.subscribe(onNext: { [unowned self] (_) in - self.animate() - }).disposed(by: self.disposeBag) + button.layer.borderColor = UIColor.black.cgColor + button.layer.borderWidth = 2 + return button }() lazy var animator1 = { - UIViewPropertyAnimator(duration: 0.5, curve: .linear) { [unowned self] in + UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) { [unowned self] in self.box1.transform = CGAffineTransform(translationX: 0, y: -100) } }() lazy var animator2 = { - UIViewPropertyAnimator(duration: 0.3, curve: .linear) { [unowned self] in - self.box2.transform = CGAffineTransform(translationX: 0, y: -100).scaledBy(x: 1.2, y: 1.2) + UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut) { [unowned self] in + self.box2.transform = CGAffineTransform(translationX: 0, y: -100) + .scaledBy(x: 1.2, y: 1.2) } }() lazy var animator3 = { - UIViewPropertyAnimator(duration: 0.2, curve: .linear) { [unowned self] in - self.box3.transform = CGAffineTransform(translationX: 0, y: -100).rotated(by: CGFloat(M_PI)) + UIViewPropertyAnimator(duration: 0.15, curve: .easeInOut) { [unowned self] in + self.box3.transform = CGAffineTransform(translationX: 0, y: -100) + .rotated(by: CGFloat(M_PI)) } }() - private func animate () { - // trigger the animation chain - self.animator1.rx.animate() - .andThen(self.animator2.rx.animate(afterDelay: 0.2)) - .andThen(self.animator3.rx.animate(afterDelay: 0.1)) - .subscribe() - .disposed(by: self.disposeBag) - } - override func viewDidLoad() { super.viewDidLoad() // construct the main view - self.view.backgroundColor = .white - self.view.addSubview(self.box1) - self.view.addSubview(self.box2) - self.view.addSubview(self.box3) - self.view.addSubview(self.button) - } + let views = [box1, box2, box3, button] + view.backgroundColor = .white + + views.forEach { + view.addSubview($0) + } + // Trigger chained animations after a button tap + button.rx.tap + .flatMap { [unowned self] in + self.animator1.rx.animate() + .andThen(self.animator2.rx.animate(afterDelay: 0.2)) + .andThen(self.animator3.rx.animate(afterDelay: 0.1)) + .debug("animation sequence") + } + .subscribe() + .disposed(by: disposeBag) + } } // Present the view controller in the Live View window From 62b3ae02f8bf763a19054d399830a245a7f45edc Mon Sep 17 00:00:00 2001 From: freak4pc Date: Wed, 4 Apr 2018 13:16:21 +0300 Subject: [PATCH 4/8] Minor test cleanup, moved availability clause to top of test to omit below iOS 10 --- .../UIViewPropertyAnimatorTests+Rx.swift | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/Tests/RxCocoa/UIViewPropertyAnimatorTests+Rx.swift b/Tests/RxCocoa/UIViewPropertyAnimatorTests+Rx.swift index e8c05ef0..f0858882 100644 --- a/Tests/RxCocoa/UIViewPropertyAnimatorTests+Rx.swift +++ b/Tests/RxCocoa/UIViewPropertyAnimatorTests+Rx.swift @@ -13,27 +13,25 @@ import RxSwiftExt import UIKit class UIViewPropertyAnimatorTests: XCTestCase { - let disposeBag = DisposeBag() + @available(iOS 10.0, *) func testAnimationCompleted() { - let expectations = expectation(description: "Animation completed") let view = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100)) - if #available(iOS 10.0, *) { - let animator = UIViewPropertyAnimator(duration: 0.5, curve: .linear) { - view.transform = CGAffineTransform(translationX: 100, y: 100) - } - animator.rx.animate().subscribe(onCompleted: { + let animator = UIViewPropertyAnimator(duration: 0.5, curve: .linear) { + view.transform = CGAffineTransform(translationX: 100, y: 100) + } + + animator + .rx.animate() + .subscribe(onCompleted: { XCTAssertEqual(100, view.frame.origin.x) XCTAssertEqual(100, view.frame.origin.y) expectations.fulfill() - }).disposed(by: self.disposeBag) - } else { - // Fallback on earlier versions - expectations.fulfill() - } + }) + .disposed(by: self.disposeBag) waitForExpectations(timeout: 1) } From e78ac14fe652afc5a78901c82f30da1894e61685 Mon Sep 17 00:00:00 2001 From: freak4pc Date: Wed, 4 Apr 2018 13:18:10 +0300 Subject: [PATCH 5/8] Update CHANGELOG --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f8c898..60b6fa9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ master 3.3.0 ----- -- added `animate(afterDelay:)` operator UIViewPropertyAnimator +- Added UIViewPropertyAnimator Reactive Extensions (`animate()` operator) 3.2.0 ----- From 1d3ed22994caa6d1028443225455ed4b01377edf Mon Sep 17 00:00:00 2001 From: freak4pc Date: Thu, 5 Apr 2018 16:44:42 +0300 Subject: [PATCH 6/8] Make sure animation without delay calls proper method --- Source/RxCocoa/UIViewPropertyAnimator+Rx.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Source/RxCocoa/UIViewPropertyAnimator+Rx.swift b/Source/RxCocoa/UIViewPropertyAnimator+Rx.swift index 44619146..1beb0fc4 100644 --- a/Source/RxCocoa/UIViewPropertyAnimator+Rx.swift +++ b/Source/RxCocoa/UIViewPropertyAnimator+Rx.swift @@ -26,7 +26,11 @@ public extension Reactive where Base: UIViewPropertyAnimator { completable(.completed) } - base.startAnimation(afterDelay: delay) + if delay != 0 { + base.startAnimation(afterDelay: delay) + } else { + base.startAnimation() + } return Disposables.create { base.stopAnimation(true) From 352b6cd1e0b9609575f6f06f75eaf4097ccda168 Mon Sep 17 00:00:00 2001 From: freak4pc Date: Thu, 5 Apr 2018 16:45:04 +0300 Subject: [PATCH 7/8] Fix playground crash by creating a fresh reversed animation as needed --- .../Contents.swift | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/Contents.swift b/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/Contents.swift index 35e60b67..16839cbe 100644 --- a/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/Contents.swift +++ b/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/Contents.swift @@ -56,25 +56,30 @@ class AnimateViewController: UIViewController { return button }() - lazy var animator1 = { - UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) { [unowned self] in - self.box1.transform = CGAffineTransform(translationX: 0, y: -100) + var animator1: UIViewPropertyAnimator! + var animator2: UIViewPropertyAnimator! + var animator3: UIViewPropertyAnimator! + + private func makeAnimators() { + animator1 = UIViewPropertyAnimator(duration: 0.3, curve: .easeInOut) { [unowned self] in + self.box1.transform = self.box1.transform != .identity ? .identity + : self.box1.transform.translatedBy(x: 0, y: -100) } - }() - lazy var animator2 = { - UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut) { [unowned self] in - self.box2.transform = CGAffineTransform(translationX: 0, y: -100) - .scaledBy(x: 1.2, y: 1.2) + animator2 = UIViewPropertyAnimator(duration: 0.25, curve: .easeInOut) { [unowned self] in + self.box2.transform = self.box2.transform != .identity ? .identity + : self.box2.transform + .translatedBy(x: 0, y: -100) + .scaledBy(x: 1.2, y: 1.2) } - }() - lazy var animator3 = { - UIViewPropertyAnimator(duration: 0.15, curve: .easeInOut) { [unowned self] in - self.box3.transform = CGAffineTransform(translationX: 0, y: -100) - .rotated(by: CGFloat(M_PI)) + animator3 = UIViewPropertyAnimator(duration: 0.15, curve: .easeInOut) { [unowned self] in + self.box3.transform = self.box3.transform != .identity ? .identity + : self.box3.transform + .translatedBy(x: 0, y: -100) + .rotated(by: .pi) } - }() + } override func viewDidLoad() { super.viewDidLoad() @@ -87,12 +92,17 @@ class AnimateViewController: UIViewController { view.addSubview($0) } + makeAnimators() + // Trigger chained animations after a button tap button.rx.tap .flatMap { [unowned self] in self.animator1.rx.animate() - .andThen(self.animator2.rx.animate(afterDelay: 0.2)) + .andThen(self.animator2.rx.animate(afterDelay: 0.15)) .andThen(self.animator3.rx.animate(afterDelay: 0.1)) + .do(onCompleted: { + self.makeAnimators() + }) .debug("animation sequence") } .subscribe() From 2c5c03ec1b3493d68a8fb58c641737f48d5ba14a Mon Sep 17 00:00:00 2001 From: freak4pc Date: Fri, 6 Apr 2018 10:24:21 +0300 Subject: [PATCH 8/8] Updated Readme --- Readme.md | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/Readme.md b/Readme.md index eb6538d4..2474c719 100644 --- a/Readme.md +++ b/Readme.md @@ -5,7 +5,7 @@ RxSwiftExt =========== -If you're using [RxSwift](https://github.com/ReactiveX/RxSwift), you may have encountered situations where the built-in operators do not bring the exact functionality you want. The RxSwift core is being intentionally kept as compact as possible to avoid bloat. This repository's purpose is to provide additional convenience operators. +If you're using [RxSwift](https://github.com/ReactiveX/RxSwift), you may have encountered situations where the built-in operators do not bring the exact functionality you want. The RxSwift core is being intentionally kept as compact as possible to avoid bloat. This repository's purpose is to provide additional convenience operators and Reactive Extensions. Installation =========== @@ -15,7 +15,6 @@ This branch of RxSwiftExt targets Swift 4.x and RxSwift 4.0.0 or later. * If you're looking for the Swift 3 version of RxSwiftExt, please use version `2.5.1` of the framework. * If your project is running on Swift 2.x, please use version `1.2` of the framework. - #### CocoaPods Using Swift 4: @@ -44,11 +43,14 @@ Add this to your `Cartfile` github "RxSwiftCommunity/RxSwiftExt" ``` - Operators =========== -RxSwiftExt is all about adding operators to [RxSwift](https://github.com/ReactiveX/RxSwift)! Currently available operators: +RxSwiftExt is all about adding operators and Reactive Extensions to [RxSwift](https://github.com/ReactiveX/RxSwift)! + +## Operators + +These operators are much like the RxSwift & RxCocoa core operators, but provide additional useful abilities to your Rx arsenal. * [unwrap](#unwrap) * [ignore](#ignore) @@ -71,13 +73,21 @@ RxSwiftExt is all about adding operators to [RxSwift](https://github.com/Reactiv * [Observable.zip(with:)](#zipwith) * [withUnretained](#withunretained) -Two additional operators are available for `materialize()`'d sequences: +There are two more available operators for `materialize()`'d sequences: * [errors](#errors-elements) * [elements](#errors-elements) Read below for details about each operator. +## Reactive Extensions + +RxSwift/RxCocoa Reactive Extensions are provided to enhance existing objects and classes from the Apple-ecosystem with Reactive abilities. + +* [UIViewPropertyAnimator.animate](#uiviewpropertyanimatoranimate) + +-------- + Operator details =========== @@ -154,7 +164,7 @@ completed Pass elements through only if they were never seen before in the sequence. ```swift - Observable.of("a","b","a","c","b","a","d") +Observable.of("a","b","a","c","b","a","d") .distinct() .subscribe { print($0) } ``` @@ -512,9 +522,9 @@ completed ``` This example emits 2, 5 (`NSDecimalNumber` Type). -### withUnretained +#### withUnretained -The `withUnretained(_:)` operator provides an unretained, safe to use (i.e. not implicitly unwrapped), reference to an object along with the events emitted by the sequence. +The `withUnretained(_:resultSelector:)` operator provides an unretained, safe to use (i.e. not implicitly unwrapped), reference to an object along with the events emitted by the sequence. In the case the provided object cannot be retained successfully, the seqeunce will complete. ```swift @@ -546,6 +556,21 @@ next((Test Class, 13)) completed ``` +Reactive Extensions details +=========== + +#### UIViewPropertyAnimator.animate + +The `animate(afterDelay:)` operator provides a Completable that triggers the animation upon subscription and completes when the animation ends. + +```swift +button.rx.tap + .flatMap { + animator1.rx.animate() + .andThen(animator2.rx.animate(afterDelay: 0.15)) + .andThen(animator3.rx.animate(afterDelay: 0.1)) + } +``` ## License This library belongs to _RxSwiftCommunity_.