PromiseKit wrapping external closure in Promises - swift

I am using an external library in Swift so I cannot control the return statements. My understanding is that I should wrap these returns in promises in order to use PromiseKit. Is this correct?
Assuming so, I have working code as follows:
private func getChannelImage(for channel: TCHChannel, completion: #escaping (UIImage?, CAProfileError?) -> Void) {
if let members = channel.members {
members.members(completion: { (result, paginator) in
if result.isSuccessful() {
// ... do something
}
else {
completion(nil, CAProfileError.UnknownError)
}
})
}
}
This can be difficult to read. I am trying to simplify this using PromiseKit. First, I want to simplify members.members(completion: { (result, paginator) in to a promise that I can call with the firstly { ... } syntax. I thus try and do as follows:
private func asPromise(members: TCHMembers) -> Promise<TCHMemberPaginator> {
return Promise<TCHMemberPaginator> { fulfill, reject in
members.members(completion: { (result, paginator) in
if result.isSuccesful() {
fulfill(paginator)
} else {
reject()
}
})
}
}
But this approach does not work and I get "Unable to infer closure type in the current context". I'm trying to find a good example of this use case done online but am having trouble. Any thoughts on how to properly return promises?

Assuming the TCHMemberPaginator and TCHMembers as below,
class TCHMemberPaginator {}
class TCHMembers {
func members(completion: (Bool, TCHMemberPaginator?) -> Void) {}
}
Here is the method to return a Promise,
private func asPromise(members: TCHMembers) -> Promise<TCHMemberPaginator> {
return Promise { seal in
members.members(completion: { (result, paginator) in
if result == true, let p = paginator {
seal.fulfill(p)
} else {
seal.reject(NSError())
}
})
}
}

Related

Observable extension with generic type

Context
I want to wrap the Alamofire.upload into an observable and having info regarding the upload progress.
For that I have created a custom UploadElement that is an enum representing either the progress and the value or the result. So far I have:
enum UploadElement<Result> where Result: Codable {
case progress(Double)
case response(Result)
}
private func buildUploadRequest(url: URL, parts: [Data]) -> Observable<UploadRequest> {
let uploadRequest = manager.upload(
multipartFormData: { multipartFormData in /* build multipart */ },
to: url
)
return Observable.just(uploadRequest)
}
func upload<Result: Codable>(url: URL, parts: [Data]) -> Observable<UploadElement<Result>> {
buildUploadRequest(url: url, parts: parts)
.flatMap { request in
Observable<UploadElement<Result>>.create { observer in
request.response { response in
do {
observer.on(.next(.response(/* decode here */)))
observer.on(.completed)
} catch let error {
observer.on(.error(error))
}
}.uploadProgress { progress in
observer.on(.next(.progress(progress.fractionCompleted)))
}
.resume()
return Disposable.create { request.cancel() }
}
}
}
Now I would like to have an extension on an Observable<UploadEment<Result>> to have a nicer way to be notified.
Basically it would be:
service.upload(url: ..., parts: ...)
.progress { progress in /* */ }
.result { result in /* */ }
.subscribe()
.dispose(by: disposeBag)
To do that I tried:
extension ObservableType where Element == UploadElement<Resource> {
func progress(progressCompletion: #escaping (Double) -> Void) -> Self {
return self.do(onNext: { element in
switch element {
case .progress(let progress): progressCompletion(progress)
case .response: return
}
})
}
func result(resultCompletion: #escaping (Result) -> Void) -> Self {
return self.do(onNext: { element in
switch element {
case .response(let result): resultCompletion(result)
case .progress: return
}
})
}
}
I tried multiple variation of that but the errors that I get are:
Cannot find 'Result in scope'
Reference to generic type ... required argument
Is it possible to achieve something like that?
You just need to move the where clause from class scope down to function scope (shown below).
That said, I don't think breaking out of the monad like this in the middle of a stream is "a nicer way to be notified".
Better would be to break your Observable into two streams and subscribe to each of them:
extension ObservableType {
func progress<Resource>() -> Observable<Double> where Element == UploadElement<Resource> {
self.compactMap { element in
switch element {
case let .progress(progress):
return progress
case .response:
return nil
}
}
}
func result<Resource>() -> Observable<Resource> where Element == UploadElement<Resource> {
self.compactMap { element in
switch element {
case .progress:
return nil
case let .response(resource):
return resource
}
}
}
}
With the above you can now do something like this:
let response = service.upload(url: ..., parts: ...)
.share()
response
.progress()
.subscribe(onNext: { progress in /*...*/ })
.disposed(by: disposeBag)
response
.result()
.subscribe(onNext: { result in /*...*/ })
.dispose(by: disposeBag)
Now you don't have any empty subscribes.
I found something that is working:
extension ObservableType {
func progress<O: Codable>(progressCompletion: #escaping (Double) -> Void) -> Observable<UploadElement<O>> where Element == UploadElement<O> {
return self.do(onNext: { element in
if case .progress(let progress) = element {
progressCompletion(progress)
}
})
}
func response<O: Codable>(responseCompletion: #escaping (O) -> Void) -> Observable<UploadElement<O>> where Element == UploadElement<O> {
return self.do(onNext: { element in
if case .response(let response) = element {
responseCompletion(response)
}
})
}
}
Now I can use the "planned" api:
service.update(data: /* ... */)
.progress { progress in /* */ }
.response { result in /* */ }
.subscribe(
onError: { error in /* */ }
)
.dispose(by: disposeBag)
However as Daniel mentioned this might not be the "nicer way of being notified".

Publisher emitting progress of operation and final value

Given I have an SDK which provides the functionality below
class SDK {
static func upload(completion: #escaping (Result<String, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion(.success("my_value"))
}
}
}
I am able to a create a wrapper around to make its usage more functional
class CombineSDK {
func upload() -> AnyPublisher<String, Error> {
Future { promise in
SDK.upload { result in
switch result {
case .success(let key):
promise(.success(key))
case .failure(let error):
promise(.failure(error))
}
}
}.eraseToAnyPublisher()
}
}
Now I'm trying to understand how my CombineSDK.upload method should look like if the SDK upload method also provides a progress block like below:
class SDK {
static func upload(progress: #escaping (Double) -> Void, completion: #escaping (Result<String, Error>) -> Void) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
progress(0.5)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
progress(1)
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
completion(.success("s3Key"))
}
}
}
We need an Output type for your publisher that represents either the progress, or the final value. So we should use an enum. Since the Foundation framework already defines a type named Progress, we'll name ours Progressable to avoid a name conflict. We might as well make it generic:
enum Progressable<Value> {
case progress(Double)
case value(Value)
}
Now we need to think about how the publisher should behave. A typical publisher, like URLSession.DataTaskPublisher, doesn't do anything until it gets a subscription, and it starts its work fresh for each subscription. The retry operator only works if the upstream publisher behaves like this.
So our publisher should behave that way, too:
extension SDK {
static func uploadPublisher() -> UploadPublisher {
return UploadPublisher()
}
struct UploadPublisher: Publisher {
typealias Output = Progressable<String>
typealias Failure = Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
<#code#>
}
}
}
Creating the publisher (by calling SDK.uploadPublisher()) doesn't start any work. We'll replace <#code#> with code to start the upload:
extension SDK {
static func uploadPublisher() -> UploadPublisher {
return UploadPublisher()
}
struct UploadPublisher: Publisher {
typealias Output = Progressable<String>
typealias Failure = Error
func receive<S>(subscriber: S) where S : Subscriber, Self.Failure == S.Failure, Self.Output == S.Input {
let subject = PassthroughSubject<Output, Failure>()
subject.receive(subscriber: subscriber)
upload(
progress: { subject.send(.progress($0)) },
completion: {
switch $0 {
case .success(let value):
subject.send(.value(value))
subject.send(completion: .finished)
case .failure(let error):
subject.send(completion: .failure(error))
}
}
)
}
}
}
Note that we call subject.receive(subscriber: subscriber) before we start the upload. This is important! What if upload calls one of its callbacks synchronously, before returning? By passing the subscriber to the subject before calling upload, we ensure that the subscriber has the chance to be notified even if upload calls its callbacks synchronously.
Note: started writing an answer that's has a largely similar intent to #robmayoff's answer, but using Deferred, so posting here for completeness.
Swift Combine only works with values and errors - there's no separate type for progress. But you can model the progress as part of the output, either as a tuple, as was suggested in another answer, or as a custom enum with both progress and result as cases, which would be my preferred approach.
class CombineSDK {
enum UploadProgress<T> {
case progress(Double)
case result(T)
}
func upload() -> AnyPublisher<UploadProgress<String>, Error> {
Deferred { () -> AnyPublisher<UploadProgress<String>, Error> in
let subject = PassthroughSubject<UploadProgress<String>, Error>()
SDK.upload(
progress: { subject.send(.progress($0)) },
completion: { r in
let _ = r.map(UploadProgress.result).publisher.subscribe(subject)
})
return subject.eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
}
EDIT
Based on #robmayoff's comment, the above solution doesn't handle synchronous case where subject.send is called before subject is returned.
The solution is largely the same, but it does introduce a small complication of having to capture these values, just in case. This can be done with Record, which will provide a temporary sink to subject
func upload() -> AnyPublisher<UploadProgress<String>, Error> {
Deferred { () -> AnyPublisher<UploadProgress<String>, Error> in
let subject = PassthroughSubject<UploadProgress<String>, Error>()
var recording = Record<UploadProgress<String>, Error>.Recording()
subject.sink(
receiveCompletion: { recording.receive(completion: $0) },
receiveValue: { recording.receive($0) })
SDK.upload(
progress: { subject.send(.progress($0)) },
completion: { r in
let _ = r.map(UploadProgress.result).publisher.subscribe(subject)
})
return Record(recording: recording).append(subject).eraseToAnyPublisher()
}
.eraseToAnyPublisher()
}
Here is possible approach
extension CombineSDK {
func upload() -> AnyPublisher<(Double, String?), Error> {
let publisher = PassthroughSubject<(Double, String?), Error>()
SDK.upload(progress: { value in
publisher.send((value, nil))
}, completion: { result in
switch result {
case .success(let key):
publisher.send((1.0, key))
publisher.send(completion: .finished)
case .failure(let error):
publisher.send(completion: .failure(error))
}
})
return publisher.eraseToAnyPublisher()
}
}

How do I reverse a promise?

I'm using PromiseKit to handle flow through a process.
Prior, I did a similar app without promises but decided frick it I'm gonna try promises just because, well, why not?
So I'm throwing a back button in the mix as I did in the prior app. Only problem is, I'm not exactly sure how to handle "reversing" if you want to call it that.
So say I have a flow of
doSomething().then {
// do something else
}.then {
// do something else
}.done {
// wrap it up, boss
}.catch {
// you're an idiot, bud
}
Say I'm in the first or second part of the chain then and I want to go back up the chain - is this possible?
Is there a link y'all can give me that I can use to read up on how to do that?
I'm thinking I might have to restart the "chain", but then how would I step through the flow....WAIT (light bulb), I can programmatically fulfill the necessary promises with whatever the data is that initially was fulfilled with until I get to the point in the "chain" where I needed to go back to, right?
Advice D:?
You can always have a catch and a then on the same promise.
var somePromise = doSomething()
// first chain
somePromise.catch { error in
// handle error
}
// second chain from the same starting point
somePromise.then {
// do something else
}.then {
// do something else
}.catch {
// you can still catch the error here too
}
You're basically creating two promise chains from the same original promise.
No, you can not do that. Once you commit a promise, you can not reverse that. Because the chain is supposed to finish in the descending order, it's cumbersome to track the order in each .then block.
What you can do is, handle the internal logic responsible to fulfill or reject a promise and start the chain from the beginning.
func executeChain() {
doSomething().then {
// do something else
}.then {
// do something else
}.done {
// condition to
executeChain()
}.catch {
// you're an idiot, bud
}
}
func doSomething() -> Promise<SomeThing>{
if (condition to bypass for reversing) {
return .value(something)
}
// Normal execution
}
But if you can improve your question with an actual use case and code then it could help providing more suitable explanation.
No you can't but you can set order in array.
bar(promises: [foo1(), foo2(), foo3()])
func bar<T>(promises: [Promise<T>]) {
when(fulfilled: promises)
.done { _ in
// TODO
}
.catch { error in
// When get error reverse array and call it again
self.bar(promises: promises.reversed())
}
}
func foo1() -> Promise<Void> {
return Promise { $0.fulfill(()) }
}
func foo2() -> Promise<Void> {
return Promise { $0.fulfill(()) }
}
func foo3() -> Promise<Void> {
return Promise { $0.fulfill(()) }
}
or alternatively
bar(foo1, foo2, foo3)
.done { _ in
// TODO
}
.catch { error in
print(error.localizedDescription)
self.bar(self.foo3, self.foo2, self.foo1)
.done { _ in
// TODO
}
.catch { error2 in
print(error2.localizedDescription)
}
}
func bar<T>(_ promise1: () -> Promise<T>,
_ promise2: #escaping () -> Promise<T>,
_ promise3: #escaping () -> Promise<T>) -> Promise<T> {
return Promise { seal in
promise1()
.then { _ in return promise2() }
.then { _ in return promise3() }
.done { model in
seal.fulfill(model)
}
.catch {
seal.reject($0)
}
}
}
func foo1() -> Promise<Void> {
return Promise { $0.fulfill(()) }
}
func foo2() -> Promise<Void> {
return Promise { $0.fulfill(()) }
}
func foo3() -> Promise<Void> {
return Promise { $0.fulfill(()) }
}

PromiseKit firstly around code, not function call

I don't want to write a separate function to return a Promise in my firstly call. I just want to write this:
firstly
{
return Promise<Bool>
{ inSeal in
var isOrderHistory = false
let importTester = CSVImporter<String>(url: url)
importTester?.startImportingRecords(structure:
{ (inFieldNames) in
if inFieldNames[2] == "Payment Instrument Type"
{
isOrderHistory = true
}
}, recordMapper: { (inRecords) -> String in
return "" // Don't care
}).onFinish
{ (inItems) in
inSeal.resolve(isOrderHistory)
}
}
}
.then
{ inIsOrderHistory in
if inIsOrderHistory -> Void
{
}
else
{
...
But I'm getting something wrong. ImportMainWindowController.swift:51:5: Ambiguous reference to member 'firstly(execute:)'
None of the example code or docs seems to cover this (what I thought was a) basic use case. In the code above, the CSVImporter operates on a background queue and calls the methods asynchronously (although in order).
I can't figure out what the full type specification should be for Promise or firstly, or what.
According to my understanding, since you are using then in the promise chain, it is also meant to return a promise and hence you are getting this error. If you intend not to return promise from your next step, you can directly use done after firstly.
Use below chain if you want to return Promise from then
firstly {
Promise<Bool> { seal in
print("hello")
seal.fulfill(true)
}
}.then { (response) in
Promise<Bool> { seal in
print(response)
seal.fulfill(true)
}
}.done { _ in
print("done")
}.catch { (error) in
print(error)
}
If you do not want to return Promise from then, you can use chain like below.
firstly {
Promise<Bool> { seal in
print("hello")
seal.fulfill(true)
}
}.done { _ in
print("done")
}.catch { (error) in
print(error)
}
I hope it helped.
Updated:
In case you do not want to return anything and then mandates to return a Promise, you can return Promise<Void> like below.
firstly {
Promise<Bool> { seal in
print("hello")
seal.fulfill(true)
}
}.then { (response) -> Promise<Void> in
print(response)
return Promise()
}.done { _ in
print("done")
}.catch { (error) in
print(error)
}

How to create a closure to return which one of the parameters is true?

I need to create some sort of closure to return back if it's an optional or forced update. I've created some pseudo code:
func verifyAppVersionWithServer(isForceUpdate: bool -> true, isOptionalUpdate: bool -> true) {
//Some check will be performed here then:
if isForceUpdate {
return isForceUpdate -> true
} else {
return isOptionalUpdate -> true
}
}
I'm not sure how to create a closure in Swift which will then return which of the parameters is true.
It is probably nicer to return an enum that indicates the type of update required.
You would then have something like this:
enum UpdateType {
case None
case Optional
case Required
}
func verifyAppVersionWithServer(completion:(UpdateType) -> Void) {
let anyUpdate = true
let forcedUpdate = false
if anyUpdate {
if forcedUpdate {
completion(.Required)
} else {
completion(.Optional)
}
} else {
completion(.None)
}
}
You would call it as:
verifyAppVersionWithServer { (updateType) in
print("Update type is \(updateType)")
}
Obviously the values would be determined by your server response, not fixed values as I have shown.
You can use something like below
func verifyAppVersionWithServer(parm1: String, withParma2: Bool, completionHandeler: (isSucess: Bool, error : NSError) -> Void) {
//Write your logic
//call complition handeler
completionHandeler(isSucess: true, error: error)
}
Hope this will help