diff --git a/CHANGELOG.md b/CHANGELOG.md index f63fd110..60b6fa9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ master ----- - added `withUnretained(_:)` operator +3.3.0 +----- +- Added UIViewPropertyAnimator Reactive Extensions (`animate()` operator) + 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..2644b501 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** [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 new file mode 100644 index 00000000..16839cbe --- /dev/null +++ b/Playground/RxSwiftExtPlayground.playground/Pages/UIViewPropertyAnimator+Rx.xcplaygroundpage/Contents.swift @@ -0,0 +1,116 @@ +/*: + > # 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` 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 { + + 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.layer.borderColor = UIColor.black.cgColor + button.layer.borderWidth = 2 + + return button + }() + + 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) + } + + 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) + } + + 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() + + // construct the main view + let views = [box1, box2, box3, button] + view.backgroundColor = .white + + views.forEach { + 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.15)) + .andThen(self.animator3.rx.animate(afterDelay: 0.1)) + .do(onCompleted: { + self.makeAnimators() + }) + .debug("animation sequence") + } + .subscribe() + .disposed(by: disposeBag) + } +} + +// 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/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_. 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..1beb0fc4 --- /dev/null +++ b/Source/RxCocoa/UIViewPropertyAnimator+Rx.swift @@ -0,0 +1,40 @@ +// +// 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(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 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 + guard position == .end else { return } + completable(.completed) + } + + if delay != 0 { + base.startAnimation(afterDelay: delay) + } else { + base.startAnimation() + } + + 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..f0858882 --- /dev/null +++ b/Tests/RxCocoa/UIViewPropertyAnimatorTests+Rx.swift @@ -0,0 +1,38 @@ +// +// 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() + + @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)) + 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) + + waitForExpectations(timeout: 1) + } +}