Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion Cartfile
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
github "mxcl/PromiseKit" ~> 6.3
#github "mxcl/PromiseKit" ~> 6.3
github "dougzilla32/PromiseKit" "PMKCancel"
#github "PromiseKit/Cancel" ~> 1.0
github "dougzilla32/Cancel" ~> 1.0
3 changes: 2 additions & 1 deletion Cartfile.resolved
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
github "AliSoftware/OHHTTPStubs" "6.1.0"
github "mxcl/PromiseKit" "6.3.3"
github "dougzilla32/Cancel" "1.0.0"
github "dougzilla32/PromiseKit" "a0217bd7b69af68237dcdeee0197e63259b9d445"
1 change: 1 addition & 0 deletions PMKFoundation.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@
inputPaths = (
PromiseKit,
OHHTTPStubs,
PMKCancel,
);
name = "Embed Carthage Frameworks";
outputPaths = (
Expand Down
5 changes: 4 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import PackageDescription
let package = Package(
name: "PMKFoundation",
dependencies: [
.Package(url: "https://github.com/mxcl/PromiseKit.git", majorVersion: 6)
.Package(url: "https://github.com/mxcl/PromiseKit.git", majorVersion: 6, minor: 3),
// Change this to point to 'PromiseKit/Cancel' before integrating
// .Package(url: "https://github.com/PromiseKit/Cancel.git", majorVersion: 1),
.Package(url: "https://github.com/dougzilla32/Cancel.git", majorVersion: 1)
],
exclude: [
"Sources/NSNotificationCenter+AnyPromise.m",
Expand Down
35 changes: 35 additions & 0 deletions Sources/NSNotificationCenter+Promise.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
#if !PMKCocoaPods
import PMKCancel
import PromiseKit
#endif

Expand Down Expand Up @@ -31,3 +32,37 @@ extension NotificationCenter {
return promise
}
}

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

extension NotificationCenter {
/// Observe the named notification once
public func observeCC(once name: Notification.Name, object: Any? = nil) -> CancellablePromise<Notification> {
let (promise, resolver) = CancellablePromise<Notification>.pending()
#if !os(Linux)
let id = addObserver(forName: name, object: object, queue: nil, using: resolver.fulfill)
#else
let id = addObserver(forName: name, object: object, queue: nil, usingBlock: resolver.fulfill)
#endif

promise.appendCancellableTask(task: ObserverTask { self.removeObserver(id) }, reject: resolver.reject)

_ = promise.ensure { self.removeObserver(id) }
return promise
}
}

class ObserverTask: CancellableTask {
let cancelBlock: () -> Void

init(cancelBlock: @escaping () -> Void) {
self.cancelBlock = cancelBlock
}

func cancel() {
cancelBlock()
isCancelled = true
}

var isCancelled = false
}
69 changes: 69 additions & 0 deletions Sources/NSObject+Promise.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
#if !PMKCocoaPods
import PMKCancel
import PromiseKit
#endif

Expand Down Expand Up @@ -55,3 +56,71 @@ private class KVOProxy: NSObject {
return Unmanaged<KVOProxy>.passUnretained(self).toOpaque()
}()
}

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

extension NSObject {
/**
- Returns: A promise that resolves when the provided keyPath changes, or when the promise is cancelled.
- Warning: *Important* The promise must not outlive the object under observation.
- SeeAlso: Apple’s KVO documentation.
*/
public func observeCC(_: PMKNamespacer, keyPath: String) -> CancellablePromise<Any?> {
var task: CancellableTask!
var reject: ((Error) -> Void)!

let promise = CancellablePromise<Any?> { seal in
reject = seal.reject
task = CancellableKVOProxy(observee: self, keyPath: keyPath, resolve: seal.fulfill)
}

promise.appendCancellableTask(task: task, reject: reject)
return promise
}
}

private class CancellableKVOProxy: NSObject, CancellableTask {
var retainCycle: CancellableKVOProxy?
let fulfill: (Any?) -> Void
let observeeObject: NSObject
let observeeKeyPath: String
var observing: Bool

@discardableResult
init(observee: NSObject, keyPath: String, resolve: @escaping (Any?) -> Void) {
fulfill = resolve
observeeObject = observee
observeeKeyPath = keyPath
observing = true
super.init()
observee.addObserver(self, forKeyPath: keyPath, options: NSKeyValueObservingOptions.new, context: pointer)
retainCycle = self
}

fileprivate override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
if let change = change, context == pointer {
defer { retainCycle = nil }
fulfill(change[NSKeyValueChangeKey.newKey])
if let object = object as? NSObject, let keyPath = keyPath, observing {
object.removeObserver(self, forKeyPath: keyPath)
observing = false
}
}
}

func cancel() {
if !isCancelled {
if observing {
observeeObject.removeObserver(self, forKeyPath: observeeKeyPath)
observing = false
}
isCancelled = true
}
}

var isCancelled = false

private lazy var pointer: UnsafeMutableRawPointer = {
return Unmanaged<CancellableKVOProxy>.passUnretained(self).toOpaque()
}()
}
185 changes: 185 additions & 0 deletions Sources/NSURLSession+Promise.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
#if !PMKCocoaPods
import PMKCancel
import PromiseKit
#endif

Expand Down Expand Up @@ -205,3 +206,187 @@ public extension Promise where T == (data: Data, response: URLResponse) {
}
}
#endif

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

extension URLSessionTask: CancellableTask {
/// `true` if the URLSessionTask was successfully cancelled, `false` otherwise
public var isCancelled: Bool {
return state == .canceling
}
}

extension URLSession {
/**
Example usage with explicit cancel context:

let context = firstly {
URLSession.shared.dataTaskCC(.promise, with: rq)
}.compactMap { data, _ in
try JSONSerialization.jsonObject(with: data) as? [String: Any]
}.then { json in
//…
}.cancelContext
//…
context.cancel()

Example usage with implicit cancel context:

let promise = firstly {
URLSession.shared.dataTaskCC(.promise, with: rq)
}.compactMap { data, _ in
try JSONSerialization.jsonObject(with: data) as? [String: Any]
}.then { json in
//…
}
//…
promise.cancel()

We recommend the use of [OMGHTTPURLRQ] which allows you to construct correct REST requests:

let context = firstly {
let rq = OMGHTTPURLRQ.POST(url, json: parameters)
URLSession.shared.dataTaskCC(.promise, with: rq)
}.then { data, urlResponse in
//…
}.cancelContext
//…
context.cancel()

We provide a convenience initializer for `String` specifically for this promise:

let context = firstly {
URLSession.shared.dataTaskCC(.promise, with: rq)
}.compactMap(String.init).then { string in
// decoded per the string encoding specified by the server
}.then { string in
print("response: string")
}
//…
context.cancel()

Other common types can be easily decoded using compactMap also:

let context = firstly {
URLSession.shared.dataTaskCC(.promise, with: rq)
}.compactMap {
UIImage(data: $0)
}.then {
self.imageView.image = $0
}
//…
context.cancel()

Though if you do decode the image this way, we recommend inflating it on a background thread
first as this will improve main thread performance when rendering the image:

let context = firstly {
URLSession.shared.dataTaskCC(.promise, with: rq)
}.compactMap(on: QoS.userInitiated) { data, _ in
guard let img = UIImage(data: data) else { return nil }
_ = cgImage?.dataProvider?.data
return img
}.then {
self.imageView.image = $0
}
//…
context.cancel()

- Parameter convertible: A URL or URLRequest.
- Returns: A cancellable promise that represents the URL request.
- SeeAlso: [OMGHTTPURLRQ]
- Remark: We deliberately don’t provide a `URLRequestConvertible` for `String` because in our experience, you should be explicit with this error path to make good apps.

[OMGHTTPURLRQ]: https://github.com/mxcl/OMGHTTPURLRQ
*/
public func dataTaskCC(_: PMKNamespacer, with convertible: URLRequestConvertible) -> CancellablePromise<(data: Data, response: URLResponse)> {
var task: URLSessionTask!
var reject: ((Error) -> Void)!

let promise = CancellablePromise<(data: Data, response: URLResponse)> {
reject = $0.reject
task = self.dataTask(with: convertible.pmkRequest, completionHandler: adapter($0))
task.resume()
}

promise.appendCancellableTask(task: task, reject: reject)
return promise
}

/// Wraps the (Data?, URLResponse?, Error?) response from URLSession.uploadTask(with:from:) as CancellablePromise<(Data,URLResponse)>
public func uploadTaskCC(_: PMKNamespacer, with convertible: URLRequestConvertible, from data: Data) -> CancellablePromise<(data: Data, response: URLResponse)> {
var task: URLSessionTask!
var reject: ((Error) -> Void)!

let promise = CancellablePromise<(data: Data, response: URLResponse)> {
reject = $0.reject
task = self.uploadTask(with: convertible.pmkRequest, from: data, completionHandler: adapter($0))
task.resume()
}

promise.appendCancellableTask(task: task, reject: reject)
return promise
}

/// Wraps the (Data?, URLResponse?, Error?) response from URLSession.uploadTask(with:fromFile:) as CancellablePromise<(Data,URLResponse)>
public func uploadTaskCC(_: PMKNamespacer, with convertible: URLRequestConvertible, fromFile file: URL) -> CancellablePromise<(data: Data, response: URLResponse)> {
var task: URLSessionTask!
var reject: ((Error) -> Void)!

let promise = CancellablePromise<(data: Data, response: URLResponse)> {
reject = $0.reject
task = self.uploadTask(with: convertible.pmkRequest, fromFile: file, completionHandler: adapter($0))
task.resume()
}

promise.appendCancellableTask(task: task, reject: reject)
return promise
}

/**
Wraps the URLSesstionDownloadTask response from URLSession.downloadTask(with:) as CancellablePromise<(URL,URLResponse)>
- Remark: we force a `to` parameter because Apple deletes the downloaded file immediately after the underyling completion handler returns.
*/
public func downloadTaskCC(_: PMKNamespacer, with convertible: URLRequestConvertible, to saveLocation: URL) -> CancellablePromise<(saveLocation: URL, response: URLResponse)> {
var task: URLSessionTask!
var reject: ((Error) -> Void)!

let promise = CancellablePromise<(saveLocation: URL, response: URLResponse)> { seal in
reject = seal.reject
task = self.downloadTask(with: convertible.pmkRequest, completionHandler: { tmp, rsp, err in
if let error = err {
seal.reject(error)
} else if let rsp = rsp, let tmp = tmp {
do {
try FileManager.default.moveItem(at: tmp, to: saveLocation)
seal.fulfill((saveLocation, rsp))
} catch {
seal.reject(error)
}
} else {
seal.reject(PMKError.invalidCallingConvention)
}
})
task.resume()
}

promise.appendCancellableTask(task: task, reject: reject)
return promise
}
}

#if swift(>=3.1)
public extension CancellablePromise where T == (data: Data, response: URLResponse) {
func validate() -> CancellablePromise<T> {
return map {
guard let response = $0.response as? HTTPURLResponse else { return $0 }
switch response.statusCode {
case 200..<300:
return $0
case let code:
throw PMKHTTPError.badStatusCode(code, $0.data, response)
}
}
}
}
#endif
37 changes: 37 additions & 0 deletions Sources/Process+Promise.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Foundation
#if !PMKCocoaPods
import PMKCancel
import PromiseKit
#endif

Expand Down Expand Up @@ -143,4 +144,40 @@ extension Process {
}
}

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

extension Process: CancellableTask {
/// Sends an interrupt signal to the process
public func cancel() {
interrupt()
}

/// `true` if the Process was successfully interrupted, `false` otherwise
public var isCancelled: Bool {
return !isRunning
}
}

extension Process {
/**
Launches the receiver and resolves when it exits, or when the promise is cancelled.

let proc = Process()
proc.launchPath = "/bin/ls"
proc.arguments = ["/bin"]
let context = proc.launchCC(.promise).compactMap { std in
String(data: std.out.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8)
}.then { stdout in
print(str)
}.cancelContext

//…

context.cancel()
*/
public func launchCC(_: PMKNamespacer) -> CancellablePromise<(out: Pipe, err: Pipe)> {
return CancellablePromise(task: self, self.launch(.promise))
}
}

#endif
Loading