diff --git a/Cartfile b/Cartfile index 2bfea98..c517d21 100644 --- a/Cartfile +++ b/Cartfile @@ -1 +1,2 @@ -github "mxcl/PromiseKit" ~> 6.0 +#github "mxcl/PromiseKit" ~> 6.0 +github "dougzilla32/PromiseKit" "CoreCancel" diff --git a/Cartfile.resolved b/Cartfile.resolved index 8d4fefc..80a4000 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1 +1 @@ -github "mxcl/PromiseKit" "6.3.4" +github "dougzilla32/PromiseKit" "087b3cf470890ff9ea841212e2f3e285fecf3988" diff --git a/Sources/HMAcessoryBrowser+Promise.swift b/Sources/HMAcessoryBrowser+Promise.swift index 06dec36..207e903 100644 --- a/Sources/HMAcessoryBrowser+Promise.swift +++ b/Sources/HMAcessoryBrowser+Promise.swift @@ -11,6 +11,8 @@ public enum HMPromiseAccessoryBrowserError: Error { public class HMPromiseAccessoryBrowser { private var proxy: BrowserProxy? + /// - Note: cancelling this promise will cancel the underlying task + /// - SeeAlso: [Cancellation](http://promisekit.org/docs/) public func start(scanInterval: ScanInterval) -> Promise<[HMAccessory]> { proxy = BrowserProxy(scanInterval: scanInterval) return proxy!.promise @@ -24,6 +26,7 @@ public class HMPromiseAccessoryBrowser { private class BrowserProxy: PromiseProxy<[HMAccessory]>, HMAccessoryBrowserDelegate { let browser = HMAccessoryBrowser() let scanInterval: ScanInterval + var timer: CancellablePromise? init(scanInterval: ScanInterval) { self.scanInterval = scanInterval @@ -40,7 +43,7 @@ private class BrowserProxy: PromiseProxy<[HMAccessory]>, HMAccessoryBrowserDeleg } if let timeout = timeout { - after(seconds: timeout) + self.timer = cancellable(after(seconds: timeout)) .done { [weak self] () -> Void in guard let _self = self else { return } _self.reject(HMPromiseAccessoryBrowserError.noAccessoryFound) @@ -60,6 +63,7 @@ private class BrowserProxy: PromiseProxy<[HMAccessory]>, HMAccessoryBrowserDeleg override func cancel() { browser.stopSearchingForNewAccessories() + timer?.cancel() super.cancel() } diff --git a/Sources/HMHomeManager+Promise.swift b/Sources/HMHomeManager+Promise.swift index a1d67fa..fff4cbb 100644 --- a/Sources/HMHomeManager+Promise.swift +++ b/Sources/HMHomeManager+Promise.swift @@ -9,6 +9,8 @@ public enum HomeKitError: Error { @available(iOS 8.0, tvOS 10.0, *) extension HMHomeManager { + /// - Note: cancelling this promise will cancel the underlying task + /// - SeeAlso: [Cancellation](http://promisekit.org/docs/) public func homes() -> Promise<[HMHome]> { return HMHomeManagerProxy().promise } @@ -43,18 +45,25 @@ extension HMHomeManager { internal class HMHomeManagerProxy: PromiseProxy<[HMHome]>, HMHomeManagerDelegate { fileprivate let manager: HMHomeManager + private var task: DispatchWorkItem! override init() { self.manager = HMHomeManager() super.init() self.manager.delegate = self - DispatchQueue.main.asyncAfter(deadline: .now() + 20.0) { [weak self] in + self.task = DispatchWorkItem { [weak self] in self?.reject(HomeKitError.permissionDeined) } + DispatchQueue.main.asyncAfter(deadline: .now() + 20.0, execute: self.task) } func homeManagerDidUpdateHomes(_ manager: HMHomeManager) { fulfill(manager.homes) } + + override func cancel() { + self.task.cancel() + super.cancel() + } } diff --git a/Sources/Utils.swift b/Sources/Utils.swift index f114789..cffba90 100644 --- a/Sources/Utils.swift +++ b/Sources/Utils.swift @@ -3,7 +3,9 @@ import PromiseKit /** Commonly used functionality when promisifying a delegate pattern */ -internal class PromiseProxy: NSObject { +internal class PromiseProxy: NSObject, CancellableTask { + var isCancelled = false + internal let (promise, seal) = Promise.pending(); private var retainCycle: PromiseProxy? @@ -13,7 +15,9 @@ internal class PromiseProxy: NSObject { // Create a retain cycle self.retainCycle = self // And ensure we break it when the promise is resolved - _ = promise.ensure { self.retainCycle = nil } + _ = promise.ensure { self.retainCycle = nil ; self.promise.setCancellableTask(nil) } + + promise.setCancellableTask(self) } /// These functions ensure we only resolve the promise once @@ -28,6 +32,7 @@ internal class PromiseProxy: NSObject { /// Cancel helper internal func cancel() { + isCancelled = true self.reject(PMKError.cancelled) } } diff --git a/Tests/HMAccessoryBrowserTests.swift b/Tests/HMAccessoryBrowserTests.swift index b6f555f..5318fb2 100644 --- a/Tests/HMAccessoryBrowserTests.swift +++ b/Tests/HMAccessoryBrowserTests.swift @@ -46,7 +46,7 @@ extension HMAccessoryBrowser { @objc func pmk_startSearchingForNewAccessories() { after(.milliseconds(100)) .done { swag in - self.delegate!.accessoryBrowser?(self, didFindNewAccessory: MockAccessory()) + self.delegate?.accessoryBrowser?(self, didFindNewAccessory: MockAccessory()) } } } @@ -81,4 +81,36 @@ func swizzle(_ foo: AnyClass, _ from: Selector, isClassMethod: Bool = false, bod method_exchangeImplementations(swizzledMethod, originalMethod) } +//////////////////////////////////////////////////////////// Cancellation + +extension HMAccessoryBrowserTests { + + func testCancelBrowserScanReturningFirst() { + swizzle(HMAccessoryBrowser.self, #selector(HMAccessoryBrowser.startSearchingForNewAccessories)) { + let ex = expectation(description: "") + + cancellable(HMPromiseAccessoryBrowser().start(scanInterval: .returnFirst(timeout: 0.5))) + .done { accessories in + XCTAssertEqual(accessories.count, 1) + XCTFail() + }.catch(policy: .allErrors) { + $0.isCancelled ? ex.fulfill() : XCTFail() + }.cancel() + + waitForExpectations(timeout: 1, handler: nil) + } + } + + func testCancelBrowserScanReturningTimeout() { + let ex = expectation(description: "") + + cancellable(HMPromiseAccessoryBrowser().start(scanInterval: .returnFirst(timeout: 0.5))) + .catch(policy: .allErrors) { + $0.isCancelled ? ex.fulfill() : XCTFail() + }.cancel() + + waitForExpectations(timeout: 1, handler: nil) + } +} + #endif