Skip to content

Commit ff69f82

Browse files
committed
'Cancel' for PromiseKit -- provides the ability to cancel promises and promise chains
1 parent 41bab77 commit ff69f82

8 files changed

+150
-4
lines changed

.travis.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ matrix:
1111
- {osx_image: xcode9.4, env: 'PLAT=iOS SWFT=3.3 DST="OS=11.4,name=iPhone 5s"'}
1212
- {osx_image: xcode9.4, env: 'PLAT=tvOS SWFT=3.3 DST="OS=11.4,name=Apple TV"'}
1313

14+
- {osx_image: xcode10, env: 'PLAT=iOS SWFT=3.4 DST="OS=12.0,name=iPhone SE"'}
15+
- {osx_image: xcode10, env: 'PLAT=tvOS SWFT=3.4 DST="OS=12.0,name=Apple TV"'}
16+
1417
- {osx_image: xcode9.2, env: 'PLAT=iOS SWFT=4.0 DST="OS=11.2,name=iPhone SE"'}
1518
- {osx_image: xcode9.2, env: 'PLAT=tvOS SWFT=4.0 DST="OS=11.2,name=Apple TV"'}
1619

@@ -21,6 +24,9 @@ matrix:
2124
- {osx_image: xcode9.3, env: 'PLAT=tvOS SWFT=4.1 DST="OS=9.2,name=Apple TV 1080p"'}
2225
- {osx_image: xcode9.3, env: 'PLAT=tvOS SWFT=4.1 DST="OS=10.2,name=Apple TV 1080p"'}
2326
- {osx_image: xcode9.4, env: 'PLAT=tvOS SWFT=4.1 DST="OS=11.4,name=Apple TV"'}
27+
28+
- {osx_image: xcode10, env: 'PLAT=iOS SWFT=4.2 DST="OS=12.0,name=iPhone SE"'}
29+
- {osx_image: xcode10, env: 'PLAT=tvOS SWFT=4.2 DST="OS=12.0,name=Apple TV"'}
2430
cache:
2531
directories:
2632
- Carthage

Cartfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
github "mxcl/PromiseKit" ~> 6.0
1+
#github "mxcl/PromiseKit" ~> 6.0
2+
github "dougzilla32/PromiseKit" "CoreCancel"

Cartfile.resolved

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
github "mxcl/PromiseKit" "6.3.3"
1+
github "dougzilla32/PromiseKit" "ff694600d4d03458121515bdc027ba76df14f7ef"

PMKUIKit.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
63C9C4581D5D339B00101ECE /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 630B2DF51D5D0AD400DC10E9 /* Default-568h@2x.png */; };
2626
63C9C45E1D5D341600101ECE /* PMKUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63C7FFA71D5BEE09003BAE60 /* PMKUIKit.framework */; };
2727
63C9C45F1D5D341600101ECE /* PMKUIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 63C7FFA71D5BEE09003BAE60 /* PMKUIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
28+
911E6FC620F571F7006F49B9 /* TestUIViewPropertyAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911E6FC520F571F7006F49B9 /* TestUIViewPropertyAnimator.swift */; };
2829
/* End PBXBuildFile section */
2930

3031
/* Begin PBXContainerItemProxy section */
@@ -95,6 +96,7 @@
9596
63C9C4451D5D334700101ECE /* PMKTestsHost.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PMKTestsHost.app; sourceTree = BUILT_PRODUCTS_DIR; };
9697
63CCF8121D5C0C4E00503216 /* Cartfile */ = {isa = PBXFileReference; lastKnownFileType = text; path = Cartfile; sourceTree = "<group>"; };
9798
63CCF8171D5C11B500503216 /* Carthage.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Carthage.xcconfig; sourceTree = "<group>"; };
99+
911E6FC520F571F7006F49B9 /* TestUIViewPropertyAnimator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestUIViewPropertyAnimator.swift; sourceTree = "<group>"; };
98100
/* End PBXFileReference section */
99101

100102
/* Begin PBXFrameworksBuildPhase section */
@@ -186,6 +188,7 @@
186188
637E2C8E1D5C2E720043E370 /* TestUIImagePickerController.swift */,
187189
637E2C8F1D5C2E720043E370 /* TestUIViewController.m */,
188190
6332142A1D83CD17009F67CE /* TestUIView.swift */,
191+
911E6FC520F571F7006F49B9 /* TestUIViewPropertyAnimator.swift */,
189192
637E2C9A1D5C2F600043E370 /* infrastructure.swift */,
190193
);
191194
path = Tests;
@@ -402,6 +405,7 @@
402405
isa = PBXSourcesBuildPhase;
403406
buildActionMask = 2147483647;
404407
files = (
408+
911E6FC620F571F7006F49B9 /* TestUIViewPropertyAnimator.swift in Sources */,
405409
637E2C951D5C2E720043E370 /* TestUIImagePickerController.swift in Sources */,
406410
637E2C961D5C2E720043E370 /* TestUIViewController.m in Sources */,
407411
637E2C9B1D5C2F600043E370 /* infrastructure.swift in Sources */,

Sources/UIImagePickerController+Promise.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,18 @@ import UIKit
66
#if !os(tvOS)
77

88
extension UIViewController {
9+
#if swift(>=4.2)
10+
/// Presents the UIImagePickerController, resolving with the user action.
11+
public func promise(_ vc: UIImagePickerController, animate: PMKAnimationOptions = [.appear, .disappear], completion: (() -> Void)? = nil) -> Promise<[UIImagePickerController.InfoKey: Any]> {
12+
let animated = animate.contains(.appear)
13+
let proxy = UIImagePickerControllerProxy()
14+
vc.delegate = proxy
15+
present(vc, animated: animated, completion: completion)
16+
return proxy.promise.ensure {
17+
vc.presentingViewController?.dismiss(animated: animated, completion: nil)
18+
}
19+
}
20+
#else
921
/// Presents the UIImagePickerController, resolving with the user action.
1022
public func promise(_ vc: UIImagePickerController, animate: PMKAnimationOptions = [.appear, .disappear], completion: (() -> Void)? = nil) -> Promise<[String: Any]> {
1123
let animated = animate.contains(.appear)
@@ -16,21 +28,33 @@ extension UIViewController {
1628
vc.presentingViewController?.dismiss(animated: animated, completion: nil)
1729
}
1830
}
31+
#endif
1932
}
2033

2134
@objc private class UIImagePickerControllerProxy: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
35+
#if swift(>=4.2)
36+
let (promise, seal) = Promise<[UIImagePickerController.InfoKey: Any]>.pending()
37+
#else
2238
let (promise, seal) = Promise<[String: Any]>.pending()
39+
#endif
2340
var retainCycle: AnyObject?
2441

2542
required override init() {
2643
super.init()
2744
retainCycle = self
2845
}
2946

47+
#if swift(>=4.2)
48+
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
49+
seal.fulfill(info)
50+
retainCycle = nil
51+
}
52+
#else
3053
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String: Any]) {
3154
seal.fulfill(info)
3255
retainCycle = nil
3356
}
57+
#endif
3458

3559
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
3660
seal.reject(UIImagePickerController.PMKError.cancelled)

Sources/UIView+Promise.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,53 @@ import PromiseKit
1919
import PromiseKit
2020
*/
2121
public extension UIView {
22+
#if swift(>=4.2)
23+
/**
24+
Animate changes to one or more views using the specified duration, delay,
25+
options, and completion handler.
26+
27+
- Parameter duration: The total duration of the animations, measured in
28+
seconds. If you specify a negative value or 0, the changes are made
29+
without animating them.
30+
31+
- Parameter delay: The amount of time (measured in seconds) to wait before
32+
beginning the animations. Specify a value of 0 to begin the animations
33+
immediately.
34+
35+
- Parameter options: A mask of options indicating how you want to perform the
36+
animations. For a list of valid constants, see UIViewAnimationOptions.
37+
38+
- Parameter animations: A block object containing the changes to commit to the
39+
views.
40+
41+
- Returns: A promise that fulfills with a boolean NSNumber indicating
42+
whether or not the animations actually finished.
43+
*/
44+
@discardableResult
45+
static func animate(_: PMKNamespacer, duration: TimeInterval, delay: TimeInterval = 0, options: UIView.AnimationOptions = [], animations: @escaping () -> Void) -> Guarantee<Bool> {
46+
return Guarantee { animate(withDuration: duration, delay: delay, options: options, animations: animations, completion: $0) }
47+
}
48+
49+
@discardableResult
50+
static func animate(_: PMKNamespacer, duration: TimeInterval, delay: TimeInterval, usingSpringWithDamping damping: CGFloat, initialSpringVelocity: CGFloat, options: UIView.AnimationOptions = [], animations: @escaping () -> Void) -> Guarantee<Bool> {
51+
return Guarantee { animate(withDuration: duration, delay: delay, usingSpringWithDamping: damping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations, completion: $0) }
52+
}
53+
54+
@discardableResult
55+
static func transition(_: PMKNamespacer, with view: UIView, duration: TimeInterval, options: UIView.AnimationOptions = [], animations: (() -> Void)?) -> Guarantee<Bool> {
56+
return Guarantee { transition(with: view, duration: duration, options: options, animations: animations, completion: $0) }
57+
}
58+
59+
@discardableResult
60+
static func transition(_: PMKNamespacer, from: UIView, to: UIView, duration: TimeInterval, options: UIView.AnimationOptions = []) -> Guarantee<Bool> {
61+
return Guarantee { transition(from: from, to: to, duration: duration, options: options, completion: $0) }
62+
}
63+
64+
@discardableResult
65+
static func perform(_: PMKNamespacer, animation: UIView.SystemAnimation, on views: [UIView], options: UIView.AnimationOptions = [], animations: (() -> Void)?) -> Guarantee<Bool> {
66+
return Guarantee { perform(animation, on: views, options: options, animations: animations, completion: $0) }
67+
}
68+
#else
2269
/**
2370
Animate changes to one or more views using the specified duration, delay,
2471
options, and completion handler.
@@ -64,4 +111,5 @@ public extension UIView {
64111
static func perform(_: PMKNamespacer, animation: UISystemAnimation, on views: [UIView], options: UIViewAnimationOptions = [], animations: (() -> Void)?) -> Guarantee<Bool> {
65112
return Guarantee { perform(animation, on: views, options: options, animations: animations, completion: $0) }
66113
}
114+
#endif
67115
}

Sources/UIViewPropertyAnimator+Promise.swift

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,28 @@ import PromiseKit
44
import UIKit
55

66
@available(iOS 10, tvOS 10, *)
7-
public extension UIViewPropertyAnimator {
7+
public extension UIViewPropertyAnimator: CancellableTask {
88
func startAnimation(_: PMKNamespacer) -> Guarantee<UIViewAnimatingPosition> {
9-
return Guarantee {
9+
return Guarantee<UIViewAnimatingPosition>(cancellableTask: self) {
1010
addCompletion($0)
1111
startAnimation()
1212
}
1313
}
14+
15+
public func cancel() {
16+
stopAnimation(true)
17+
}
18+
19+
public var isCancelled: Bool {
20+
return (state == .inactive) && (fractionComplete < 1.0)
21+
}
22+
}
23+
24+
//////////////////////////////////////////////////////////// Cancellable wrapper
25+
26+
@available(iOS 10, tvOS 10, *)
27+
extension UIViewPropertyAnimator {
28+
public func cancellableStartAnimation(_: PMKNamespacer) -> CancellablePromise<UIViewAnimatingPosition> {
29+
return cancellable(startAnimation(.promise))
30+
}
1431
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import PromiseKit
2+
import PMKUIKit
3+
import XCTest
4+
import UIKit
5+
6+
@available(iOS 10, tvOS 10, *)
7+
class UIViewPropertyAnimatorTests: XCTestCase {
8+
func test() {
9+
let ex1 = expectation(description: "")
10+
let ex2 = expectation(description: "")
11+
12+
let animator = UIViewPropertyAnimator(duration: 0.1, curve: .easeIn, animations: { [weak self] in
13+
ex1.fulfill()
14+
})
15+
animator.startAnimation(.promise).done { _ in
16+
ex2.fulfill()
17+
}
18+
19+
waitForExpectations(timeout: 1)
20+
}
21+
}
22+
23+
//////////////////////////////////////////////////////////// Cancellation
24+
25+
@available(iOS 10, tvOS 10, *)
26+
extension UIViewPropertyAnimatorTests {
27+
func testCancel() {
28+
let ex1 = expectation(description: "")
29+
let ex2 = expectation(description: "")
30+
31+
let animator = UIViewPropertyAnimator(duration: 0.1, curve: .easeIn, animations: { [weak self] in
32+
ex1.fulfill()
33+
})
34+
let p = animator.cancellableStartAnimation(.promise).done { _ in
35+
XCTFail()
36+
}.catch(policy: .allErrors) { error in
37+
error.isCancelled ? ex2.fulfill() : XCTFail("Error: \(error)")
38+
}
39+
p.cancel()
40+
41+
XCTAssert(animator.isCancelled)
42+
XCTAssert(p.isCancelled)
43+
44+
waitForExpectations(timeout: 1)
45+
}
46+
}

0 commit comments

Comments
 (0)