* Additional comments for Dispatcher.swift
* Remove .promise namespacer from Dispatcher.dispatch -> Guarantee/Promise
* Convert to LogEvent system, groom logging and tests
* Expand DispatchQueueDispatcher to hold both a DispatchGroup and a QoS
* Finalize dispatcher.dispatch and related tests
* Clean up DispatchQueue wrapping, avoid assuming any particular defaults
* CurrentThreadDispatcher.dispatch needn't mark closure arg as @escaping
* Test that DispatchQueue.pmkDefault is identifiable with ===
* Documentation updates
* Build any v7* branch
* Rebuild Linux test manifest, check return types of Dispatcher.dispatch {}
7.4 KiB
Common Misusage
Doubling up Promises
Don’t do this:
func toggleNetworkSpinnerWithPromise<T>(funcToCall: () -> Promise<T>) -> Promise<T> {
return Promise { seal in
firstly {
setNetworkActivityIndicatorVisible(true)
return funcToCall()
}.then { result in
seal.fulfill(result)
}.always {
setNetworkActivityIndicatorVisible(false)
}.catch { err in
seal.reject(err)
}
}
}
Do this:
func toggleNetworkSpinnerWithPromise<T>(funcToCall: () -> Promise<T>) -> Promise<T> {
return firstly {
setNetworkActivityIndicatorVisible(true)
return funcToCall()
}.always {
setNetworkActivityIndicatorVisible(false)
}
}
You already had a promise, you don’t need to wrap it in another promise.
Optionals in Promises
When we see Promise<Item?>, it usually implies a misuse of promises. For
example:
return firstly {
getItems()
}.then { items -> Promise<[Item]?> in
guard !items.isEmpty else {
return .value(nil)
}
return Promise(value: items)
}
The second then chooses to return nil in some circumstances. This choice
imposes the need to check for nil on the consumer of the promise.
It's usually better to shunt these sorts of exceptions away from the happy path and onto the error path. In this case, we can create a specific error type for this condition:
return firstly {
getItems()
}.map { items -> [Item]> in
guard !items.isEmpty else {
throw MyError.emptyItems
}
return items
}
Note
: Use
compactMapwhen an API outside your control returns an Optional and you want to generate an error instead of propagatingnil.
Tips n’ Tricks
Background-Loaded Member Variables
class MyViewController: UIViewController {
private let ambience: Promise<AVAudioPlayer> = DispatchQueue.global().async(.promise) {
guard let asset = NSDataAsset(name: "CreepyPad") else { throw PMKError.badInput }
let player = try AVAudioPlayer(data: asset.data)
player.prepareToPlay()
return player
}
}
Chaining Animations
firstly {
UIView.animate(.promise, duration: 0.3) {
self.button1.alpha = 0
}
}.then {
UIView.animate(.promise, duration: 0.3) {
self.button2.alpha = 1
}
}.then {
UIView.animate(.promise, duration: 0.3) {
adjustConstraints()
self.view.layoutIfNeeded()
}
}
Voiding Promises
It is often convenient to erase the type of a promise to facilitate chaining.
For example, UIView.animate(.promise) returns Guarantee<Bool> because UIKit’s
completion API supplies a Bool. However, we usually don’t need this value and
can chain more simply if it is discarded (that is, converted to Void). We can use
asVoid() to achieve this conversion:
UIView.animate(.promise, duration: 0.3) {
self.button1.alpha = 0
}.asVoid().done(self.nextStep)
For situations in which we are combining many promises into a when, asVoid()
becomes essential:
let p1 = foo()
let p2 = bar()
let p3 = baz()
//…
let p10 = fluff()
when(fulfilled: p1.asVoid(), p2.asVoid(), /*…*/, p10.asVoid()).then {
let value1 = p1.value! // safe bang since all the promises fulfilled
// …
let value10 = p10.value!
}.catch {
//…
}
You normally don't have to do this explicitly because when does it for you
for up to 5 parameters.
Blocking (Await)
Sometimes you have to block the main thread to await completion of an asynchronous task.
In these cases, you can (with caution) use wait:
public extension UNUserNotificationCenter {
var wasPushRequested: Bool {
let settings = Guarantee(resolver: getNotificationSettings).wait()
return settings != .notDetermined
}
}
The task under the promise must not call back onto the current thread or it will deadlock.
Starting a Chain on a Background Queue/Thread
firstly deliberately does not take a queue. A detailed rationale for this choice
can be found in the ticket tracker.
So, if you want to start a chain by dispatching to the background, you have to use
DispatchQueue.async:
DispatchQueue.global().async(.promise) {
return value
}.done { value in
//…
}
However, this function cannot return a promise because of Swift compiler ambiguity issues. Thus, if you must start a promise on a background queue, you need to do something like this:
Promise { seal in
DispatchQueue.global().async {
seal(value)
}
}.done { value in
//…
}
Or more simply (though with caveats; see the documentation for wait):
DispatchQueue.global().async(.promise) {
return try fetch().wait()
}.done { value in
//…
}
However, you shouldn't need to do this often. If you find yourself wanting to use
this technique, perhaps you should instead modify the code for fetch to make it do
its work on a background thread.
Promises abstract asynchronicity, so exploit and support that model. Design your APIs so that consumers don’t have to care what queue your functions run on.
Dispatcher Objects
Some issues are best addressed at the level of dispatching. For example, you may need to perform a series of HTTP transactions on a server that allows only a certain number of API calls per minute. Or, you might want to allow only a certain number of memory-hungry background tasks to run at once. These aren't really promise-level concerns; they just have to do with the details of how closures are invoked once they become eligible to run.
Unfortunately, DispatchQueues don't directly support these types of restrictions, and because
of the way DispatchQueue is implemented, you can't create your own subclasses. PromiseKit 7
adds an abstract Dispatcher protocol that generalizes the idea of a dispatch queue and allows for
alternate implementations:
public protocol Dispatcher {
func dispatch(_ body: @escaping () -> Void)
}
Anywhere in PromiseKit that you can use a DispatchQueue, you're free to substitute a Dispatcher.
PromiseKit doesn't care how you implement dispatch(). You can run the provided closure synchronously or
asynchronously, now or at some point in the future, on any thread you wish. Of course, your own code
must have a thread safety strategy and not create deadlocks.
If you're setting a default dispatcher, assign your dispatcher to PromiseKit.conf.D rather than PromiseKit.conf.Q; the latter
accepts only DispatchQueues.
FIXME: Check locations and availability of Dispatcher implementations before release
A few handy Dispatcher types are available in the Dispatchers extension library:
RateLimitedDispatcherimplements general dispatch rate limits with an approximate "token bucket" strategy.StrictRateLimitedDispatcheris an exact and optimal "sliding window" rate limiter, but requires O(n) space (n = # of events/time)ConcurrencyLimitedDispatcherallows only n asynchronous closures to run at once.CoreDataDispatcherlets you dispatch onto threads associated withNSManagedObjectContexts.
A couple of Dispatchers are also included in the PromiseKit core:
CurrentThreadDispatcherruns closures immediately, on the current thread.DispatchQueueDispatcherforwards to aDispatchQueue, applying a staticDispatchGroup, quality of service, and flag set.