Skip to content

Commit

Permalink
'Cancel' for PromiseKit -- provides the ability to cancel promises an…
Browse files Browse the repository at this point in the history
…d promise chains
  • Loading branch information
dougzilla32 committed Sep 12, 2018
1 parent 41bab77 commit ff69f82
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 4 deletions.
6 changes: 6 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ matrix:
- {osx_image: xcode9.4, env: 'PLAT=iOS SWFT=3.3 DST="OS=11.4,name=iPhone 5s"'}
- {osx_image: xcode9.4, env: 'PLAT=tvOS SWFT=3.3 DST="OS=11.4,name=Apple TV"'}

- {osx_image: xcode10, env: 'PLAT=iOS SWFT=3.4 DST="OS=12.0,name=iPhone SE"'}
- {osx_image: xcode10, env: 'PLAT=tvOS SWFT=3.4 DST="OS=12.0,name=Apple TV"'}

- {osx_image: xcode9.2, env: 'PLAT=iOS SWFT=4.0 DST="OS=11.2,name=iPhone SE"'}
- {osx_image: xcode9.2, env: 'PLAT=tvOS SWFT=4.0 DST="OS=11.2,name=Apple TV"'}

Expand All @@ -21,6 +24,9 @@ matrix:
- {osx_image: xcode9.3, env: 'PLAT=tvOS SWFT=4.1 DST="OS=9.2,name=Apple TV 1080p"'}
- {osx_image: xcode9.3, env: 'PLAT=tvOS SWFT=4.1 DST="OS=10.2,name=Apple TV 1080p"'}
- {osx_image: xcode9.4, env: 'PLAT=tvOS SWFT=4.1 DST="OS=11.4,name=Apple TV"'}

- {osx_image: xcode10, env: 'PLAT=iOS SWFT=4.2 DST="OS=12.0,name=iPhone SE"'}
- {osx_image: xcode10, env: 'PLAT=tvOS SWFT=4.2 DST="OS=12.0,name=Apple TV"'}
cache:
directories:
- Carthage
Expand Down
3 changes: 2 additions & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
github "mxcl/PromiseKit" ~> 6.0
#github "mxcl/PromiseKit" ~> 6.0
github "dougzilla32/PromiseKit" "CoreCancel"
2 changes: 1 addition & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1 +1 @@
github "mxcl/PromiseKit" "6.3.3"
github "dougzilla32/PromiseKit" "ff694600d4d03458121515bdc027ba76df14f7ef"
4 changes: 4 additions & 0 deletions PMKUIKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
63C9C4581D5D339B00101ECE /* Default-568h@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 630B2DF51D5D0AD400DC10E9 /* Default-568h@2x.png */; };
63C9C45E1D5D341600101ECE /* PMKUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 63C7FFA71D5BEE09003BAE60 /* PMKUIKit.framework */; };
63C9C45F1D5D341600101ECE /* PMKUIKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 63C7FFA71D5BEE09003BAE60 /* PMKUIKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
911E6FC620F571F7006F49B9 /* TestUIViewPropertyAnimator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911E6FC520F571F7006F49B9 /* TestUIViewPropertyAnimator.swift */; };
/* End PBXBuildFile section */

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

/* Begin PBXFrameworksBuildPhase section */
Expand Down Expand Up @@ -186,6 +188,7 @@
637E2C8E1D5C2E720043E370 /* TestUIImagePickerController.swift */,
637E2C8F1D5C2E720043E370 /* TestUIViewController.m */,
6332142A1D83CD17009F67CE /* TestUIView.swift */,
911E6FC520F571F7006F49B9 /* TestUIViewPropertyAnimator.swift */,
637E2C9A1D5C2F600043E370 /* infrastructure.swift */,
);
path = Tests;
Expand Down Expand Up @@ -402,6 +405,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
911E6FC620F571F7006F49B9 /* TestUIViewPropertyAnimator.swift in Sources */,
637E2C951D5C2E720043E370 /* TestUIImagePickerController.swift in Sources */,
637E2C961D5C2E720043E370 /* TestUIViewController.m in Sources */,
637E2C9B1D5C2F600043E370 /* infrastructure.swift in Sources */,
Expand Down
24 changes: 24 additions & 0 deletions Sources/UIImagePickerController+Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@ import UIKit
#if !os(tvOS)

extension UIViewController {
#if swift(>=4.2)
/// Presents the UIImagePickerController, resolving with the user action.
public func promise(_ vc: UIImagePickerController, animate: PMKAnimationOptions = [.appear, .disappear], completion: (() -> Void)? = nil) -> Promise<[UIImagePickerController.InfoKey: Any]> {
let animated = animate.contains(.appear)
let proxy = UIImagePickerControllerProxy()
vc.delegate = proxy
present(vc, animated: animated, completion: completion)
return proxy.promise.ensure {
vc.presentingViewController?.dismiss(animated: animated, completion: nil)
}
}
#else
/// Presents the UIImagePickerController, resolving with the user action.
public func promise(_ vc: UIImagePickerController, animate: PMKAnimationOptions = [.appear, .disappear], completion: (() -> Void)? = nil) -> Promise<[String: Any]> {
let animated = animate.contains(.appear)
Expand All @@ -16,21 +28,33 @@ extension UIViewController {
vc.presentingViewController?.dismiss(animated: animated, completion: nil)
}
}
#endif
}

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

required override init() {
super.init()
retainCycle = self
}

#if swift(>=4.2)
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
seal.fulfill(info)
retainCycle = nil
}
#else
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String: Any]) {
seal.fulfill(info)
retainCycle = nil
}
#endif

func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
seal.reject(UIImagePickerController.PMKError.cancelled)
Expand Down
48 changes: 48 additions & 0 deletions Sources/UIView+Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,53 @@ import PromiseKit
import PromiseKit
*/
public extension UIView {
#if swift(>=4.2)
/**
Animate changes to one or more views using the specified duration, delay,
options, and completion handler.
- Parameter duration: The total duration of the animations, measured in
seconds. If you specify a negative value or 0, the changes are made
without animating them.
- Parameter delay: The amount of time (measured in seconds) to wait before
beginning the animations. Specify a value of 0 to begin the animations
immediately.
- Parameter options: A mask of options indicating how you want to perform the
animations. For a list of valid constants, see UIViewAnimationOptions.
- Parameter animations: A block object containing the changes to commit to the
views.
- Returns: A promise that fulfills with a boolean NSNumber indicating
whether or not the animations actually finished.
*/
@discardableResult
static func animate(_: PMKNamespacer, duration: TimeInterval, delay: TimeInterval = 0, options: UIView.AnimationOptions = [], animations: @escaping () -> Void) -> Guarantee<Bool> {
return Guarantee { animate(withDuration: duration, delay: delay, options: options, animations: animations, completion: $0) }
}

@discardableResult
static func animate(_: PMKNamespacer, duration: TimeInterval, delay: TimeInterval, usingSpringWithDamping damping: CGFloat, initialSpringVelocity: CGFloat, options: UIView.AnimationOptions = [], animations: @escaping () -> Void) -> Guarantee<Bool> {
return Guarantee { animate(withDuration: duration, delay: delay, usingSpringWithDamping: damping, initialSpringVelocity: initialSpringVelocity, options: options, animations: animations, completion: $0) }
}

@discardableResult
static func transition(_: PMKNamespacer, with view: UIView, duration: TimeInterval, options: UIView.AnimationOptions = [], animations: (() -> Void)?) -> Guarantee<Bool> {
return Guarantee { transition(with: view, duration: duration, options: options, animations: animations, completion: $0) }
}

@discardableResult
static func transition(_: PMKNamespacer, from: UIView, to: UIView, duration: TimeInterval, options: UIView.AnimationOptions = []) -> Guarantee<Bool> {
return Guarantee { transition(from: from, to: to, duration: duration, options: options, completion: $0) }
}

@discardableResult
static func perform(_: PMKNamespacer, animation: UIView.SystemAnimation, on views: [UIView], options: UIView.AnimationOptions = [], animations: (() -> Void)?) -> Guarantee<Bool> {
return Guarantee { perform(animation, on: views, options: options, animations: animations, completion: $0) }
}
#else
/**
Animate changes to one or more views using the specified duration, delay,
options, and completion handler.
Expand Down Expand Up @@ -64,4 +111,5 @@ public extension UIView {
static func perform(_: PMKNamespacer, animation: UISystemAnimation, on views: [UIView], options: UIViewAnimationOptions = [], animations: (() -> Void)?) -> Guarantee<Bool> {
return Guarantee { perform(animation, on: views, options: options, animations: animations, completion: $0) }
}
#endif
}
21 changes: 19 additions & 2 deletions Sources/UIViewPropertyAnimator+Promise.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,28 @@ import PromiseKit
import UIKit

@available(iOS 10, tvOS 10, *)
public extension UIViewPropertyAnimator {
public extension UIViewPropertyAnimator: CancellableTask {
func startAnimation(_: PMKNamespacer) -> Guarantee<UIViewAnimatingPosition> {
return Guarantee {
return Guarantee<UIViewAnimatingPosition>(cancellableTask: self) {
addCompletion($0)
startAnimation()
}
}

public func cancel() {
stopAnimation(true)
}

public var isCancelled: Bool {
return (state == .inactive) && (fractionComplete < 1.0)
}
}

//////////////////////////////////////////////////////////// Cancellable wrapper

@available(iOS 10, tvOS 10, *)
extension UIViewPropertyAnimator {
public func cancellableStartAnimation(_: PMKNamespacer) -> CancellablePromise<UIViewAnimatingPosition> {
return cancellable(startAnimation(.promise))
}
}
46 changes: 46 additions & 0 deletions Tests/TestUIViewPropertyAnimator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import PromiseKit
import PMKUIKit
import XCTest
import UIKit

@available(iOS 10, tvOS 10, *)
class UIViewPropertyAnimatorTests: XCTestCase {
func test() {
let ex1 = expectation(description: "")
let ex2 = expectation(description: "")

let animator = UIViewPropertyAnimator(duration: 0.1, curve: .easeIn, animations: { [weak self] in
ex1.fulfill()
})
animator.startAnimation(.promise).done { _ in
ex2.fulfill()
}

waitForExpectations(timeout: 1)
}
}

//////////////////////////////////////////////////////////// Cancellation

@available(iOS 10, tvOS 10, *)
extension UIViewPropertyAnimatorTests {
func testCancel() {
let ex1 = expectation(description: "")
let ex2 = expectation(description: "")

let animator = UIViewPropertyAnimator(duration: 0.1, curve: .easeIn, animations: { [weak self] in
ex1.fulfill()
})
let p = animator.cancellableStartAnimation(.promise).done { _ in
XCTFail()
}.catch(policy: .allErrors) { error in
error.isCancelled ? ex2.fulfill() : XCTFail("Error: \(error)")
}
p.cancel()

XCTAssert(animator.isCancelled)
XCTAssert(p.isCancelled)

waitForExpectations(timeout: 1)
}
}

0 comments on commit ff69f82

Please sign in to comment.