Skip to content

Commit 6bac29c

Browse files
committed
'Cancel' for PromiseKit -- provides the ability to cancel promises and promise chains
1 parent 1278bb2 commit 6bac29c

File tree

5 files changed

+235
-4
lines changed

5 files changed

+235
-4
lines changed

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" "288f7fbabc0b33c558bf908a3a0770693223d4e0"

Sources/CLLocationManager+Promise.swift

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ extension CLLocationManager {
9797
}
9898
}
9999

100-
private class LocationManager: CLLocationManager, CLLocationManagerDelegate {
100+
private class LocationManager: CLLocationManager, CLLocationManagerDelegate, CancellableTask {
101101
let (promise, seal) = Promise<[CLLocation]>.pending()
102102
let satisfyingBlock: ((CLLocation) -> Bool)?
103103

@@ -120,6 +120,9 @@ private class LocationManager: CLLocationManager, CLLocationManagerDelegate {
120120
satisfyingBlock = block
121121
super.init()
122122
delegate = self
123+
124+
promise.setCancellableTask(self, reject: seal.reject)
125+
123126
#if !os(tvOS)
124127
startUpdatingLocation()
125128
#else
@@ -138,6 +141,13 @@ private class LocationManager: CLLocationManager, CLLocationManagerDelegate {
138141
seal.reject(error)
139142
}
140143
}
144+
145+
func cancel() {
146+
self.stopUpdatingLocation()
147+
isCancelled = true
148+
}
149+
150+
var isCancelled = false
141151
}
142152

143153

@@ -203,14 +213,16 @@ extension CLLocationManager {
203213
}
204214

205215
@available(iOS 8, *)
206-
private class AuthorizationCatcher: CLLocationManager, CLLocationManagerDelegate {
216+
private class AuthorizationCatcher: CLLocationManager, CLLocationManagerDelegate, CancellableTask {
207217
let (promise, fulfill) = Guarantee<CLAuthorizationStatus>.pending()
208218
var retainCycle: AuthorizationCatcher?
209219
let initialAuthorizationState = CLLocationManager.authorizationStatus()
210220

211221
init(type: PMKCLAuthorizationType) {
212222
super.init()
213223

224+
promise.setCancellableTask(self)
225+
214226
func ask(type: PMKCLAuthorizationType) {
215227
delegate = self
216228
retainCycle = self
@@ -263,6 +275,13 @@ private class AuthorizationCatcher: CLLocationManager, CLLocationManagerDelegate
263275
fulfill(status)
264276
}
265277
}
278+
279+
func cancel() {
280+
self.retainCycle = nil
281+
isCancelled = true
282+
}
283+
284+
var isCancelled = false
266285
}
267286

268287
#endif
@@ -305,3 +324,43 @@ private enum PMKCLAuthorizationType {
305324
case always
306325
case whenInUse
307326
}
327+
328+
//////////////////////////////////////////////////////////// Cancellable wrappers
329+
330+
extension CLLocationManager {
331+
/**
332+
Request the current location, with the ability to cancel the request.
333+
- Note: to obtain a single location use `Promise.lastValue`
334+
- Parameters:
335+
- authorizationType: requestAuthorizationType: We read your Info plist and try to
336+
determine the authorization type we should request automatically. If you
337+
want to force one or the other, change this parameter from its default
338+
value.
339+
- block: A block by which to perform any filtering of the locations that are
340+
returned. In order to only retrieve accurate locations, only return true if the
341+
locations horizontal accuracy < 50
342+
- Returns: A new promise that fulfills with the most recent CLLocation that satisfies
343+
the provided block if it exists. If the block does not exist, simply return the
344+
last location.
345+
*/
346+
public class func cancellableRequestLocation(authorizationType: RequestAuthorizationType = .automatic, satisfying block: ((CLLocation) -> Bool)? = nil) -> CancellablePromise<[CLLocation]> {
347+
return cancellable(requestLocation(authorizationType: authorizationType, satisfying: block))
348+
}
349+
}
350+
351+
352+
#if !os(macOS)
353+
354+
extension CLLocationManager {
355+
/**
356+
Request CoreLocation authorization from the user
357+
- Note: By default we try to determine the authorization type you want by inspecting your Info.plist
358+
- Note: This method will not perform upgrades from “when-in-use” to “always” unless you specify `.always` for the value of `type`.
359+
*/
360+
@available(iOS 8, tvOS 9, watchOS 2, *)
361+
public class func cancellableRequestAuthorization(type requestedAuthorizationType: RequestAuthorizationType = .automatic) -> CancellablePromise<CLAuthorizationStatus> {
362+
return cancellable(requestAuthorization(type: requestedAuthorizationType))
363+
}
364+
}
365+
366+
#endif

Tests/CLGeocoderTests.swift

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,3 +100,117 @@ class CLGeocoderTests: XCTestCase {
100100
}
101101

102102
private let dummyPlacemark = CLPlacemark()
103+
104+
//////////////////////////////////////////////////////////// Cancellation
105+
106+
extension CLGeocoderTests {
107+
func testCancel_reverseGeocodeLocation() {
108+
class MockGeocoder: CLGeocoder {
109+
override func reverseGeocodeLocation(_ location: CLLocation, completionHandler: @escaping CLGeocodeCompletionHandler) {
110+
after(.milliseconds(100)).done {
111+
completionHandler([dummyPlacemark], nil)
112+
}
113+
}
114+
}
115+
116+
let ex = expectation(description: "")
117+
cancellable(MockGeocoder().reverseGeocode(location: CLLocation())).done { _ in
118+
XCTFail("not cancelled")
119+
}.catch(policy: .allErrors) { error in
120+
error.isCancelled ? ex.fulfill() : XCTFail("error \(error)")
121+
}.cancel()
122+
123+
waitForExpectations(timeout: 1)
124+
}
125+
126+
func testCancel_geocodeAddressDictionary() {
127+
class MockGeocoder: CLGeocoder {
128+
override func geocodeAddressDictionary(_ addressDictionary: [AnyHashable: Any], completionHandler: @escaping CLGeocodeCompletionHandler) {
129+
after(.milliseconds(100)).done {
130+
completionHandler([dummyPlacemark], nil)
131+
}
132+
}
133+
}
134+
135+
let ex = expectation(description: "")
136+
let context = cancellable(MockGeocoder().geocode([:])).done { _ in
137+
XCTFail("not cancelled")
138+
}.catch(policy: .allErrors) { error in
139+
error.isCancelled ? ex.fulfill() : XCTFail("error \(error)")
140+
}.cancelContext
141+
after(.milliseconds(50)).done {
142+
context.cancel()
143+
}
144+
145+
waitForExpectations(timeout: 1)
146+
}
147+
148+
func testCancel_geocodeAddressString() {
149+
class MockGeocoder: CLGeocoder {
150+
override func geocodeAddressString(_ addressString: String, completionHandler: @escaping CLGeocodeCompletionHandler) {
151+
after(.milliseconds(100)).done {
152+
completionHandler([dummyPlacemark], nil)
153+
}
154+
}
155+
}
156+
157+
let ex = expectation(description: "")
158+
let p = cancellable(MockGeocoder().geocode("")).done { _ in
159+
XCTFail("not cancelled")
160+
}.catch(policy: .allErrors) { error in
161+
error.isCancelled ? ex.fulfill() : XCTFail("error \(error)")
162+
}
163+
after(.milliseconds(50)).done {
164+
p.cancel()
165+
}
166+
waitForExpectations(timeout: 1)
167+
}
168+
169+
#if !os(tvOS) && swift(>=3.2)
170+
func testCancel_geocodePostalAddress() {
171+
guard #available(iOS 11.0, OSX 10.13, watchOS 4.0, *) else { return }
172+
173+
class MockGeocoder: CLGeocoder {
174+
override func geocodePostalAddress(_ postalAddress: CNPostalAddress, completionHandler: @escaping CLGeocodeCompletionHandler) {
175+
after(.milliseconds(100)).done {
176+
completionHandler([dummyPlacemark], nil)
177+
}
178+
}
179+
}
180+
181+
let ex = expectation(description: "")
182+
let p = cancellable(MockGeocoder().geocodePostalAddress(CNPostalAddress())).done { _ in
183+
XCTFail("not cancelled")
184+
}.catch(policy: .allErrors) { error in
185+
error.isCancelled ? ex.fulfill() : XCTFail("error \(error)")
186+
}
187+
after(.milliseconds(50)).done {
188+
p.cancel()
189+
}
190+
waitForExpectations(timeout: 1)
191+
}
192+
193+
func testCancel_geocodePostalAddressLocale() {
194+
guard #available(iOS 11.0, OSX 10.13, watchOS 4.0, *) else { return }
195+
196+
class MockGeocoder: CLGeocoder {
197+
override func geocodePostalAddress(_ postalAddress: CNPostalAddress, preferredLocale locale: Locale?, completionHandler: @escaping CLGeocodeCompletionHandler) {
198+
after(.milliseconds(100)).done {
199+
completionHandler([dummyPlacemark], nil)
200+
}
201+
}
202+
}
203+
204+
let ex = expectation(description: "")
205+
let p = cancellable(MockGeocoder().geocodePostalAddress(CNPostalAddress(), preferredLocale: nil)).done { _ in
206+
XCTFail("not cancelled")
207+
}.catch(policy: .allErrors) { error in
208+
error.isCancelled ? ex.fulfill() : XCTFail("error \(error)")
209+
}
210+
after(.milliseconds(50)).done {
211+
p.cancel()
212+
}
213+
waitForExpectations(timeout: 1)
214+
}
215+
#endif
216+
}

Tests/CLLocationManagerTests.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,63 @@ class Test_CLLocationManager_Swift: XCTestCase {
5151
#endif
5252
}
5353

54+
//////////////////////////////////////////////////////////// Cancellation
55+
56+
extension Test_CLLocationManager_Swift {
57+
func testCancel_fulfills_with_multiple_locations() {
58+
swizzle(CLLocationManager.self, #selector(CLLocationManager.startUpdatingLocation)) {
59+
swizzle(CLLocationManager.self, #selector(CLLocationManager.authorizationStatus), isClassMethod: true) {
60+
let ex = expectation(description: "")
61+
62+
let p = cancellable(CLLocationManager.requestLocation()).done { _ in
63+
XCTFail("not cancelled")
64+
}.catch(policy: .allErrors) { error in
65+
error.isCancelled ? ex.fulfill() : XCTFail("error \(error)")
66+
}
67+
after(.milliseconds(50)).done {
68+
p.cancel()
69+
}
70+
71+
waitForExpectations(timeout: 1)
72+
}
73+
}
74+
}
75+
76+
func testCancel_fufillsWithSatisfyingBlock() {
77+
swizzle(CLLocationManager.self, #selector(CLLocationManager.startUpdatingLocation)) {
78+
swizzle(CLLocationManager.self, #selector(CLLocationManager.authorizationStatus), isClassMethod: true) {
79+
let ex = expectation(description: "")
80+
let block: ((CLLocation) -> Bool) = { location in
81+
return location.coordinate.latitude == dummy.last?.coordinate.latitude
82+
}
83+
let p = CLLocationManager.cancellableRequestLocation(satisfying: block).done { _ in
84+
XCTFail("not cancelled")
85+
}.catch(policy: .allErrors) { error in
86+
error.isCancelled ? ex.fulfill() : XCTFail("error \(error)")
87+
}
88+
after(.milliseconds(50)).done {
89+
p.cancel()
90+
}
91+
waitForExpectations(timeout: 1)
92+
}
93+
}
94+
}
95+
96+
#if os(iOS)
97+
func testCancel_requestAuthorization() {
98+
let ex = expectation(description: "")
99+
100+
let p = CLLocationManager.cancellableRequestAuthorization().done { _ in
101+
XCTFail("not cancelled")
102+
}.catch(policy: .allErrors) { error in
103+
error.isCancelled ? ex.fulfill() : XCTFail("error \(error)")
104+
}
105+
p.cancel()
106+
107+
waitForExpectations(timeout: 1, handler: nil)
108+
}
109+
#endif
110+
}
54111

55112
/////////////////////////////////////////////////////////////// resources
56113
private let dummy = [CLLocation(latitude: 0, longitude: 0), CLLocation(latitude: 10, longitude: 20)]

0 commit comments

Comments
 (0)