I am trying to implement DispatchGroup as follows, but if the first call returns true, then the second one returns false, then overall result will return false.
However, if the first call returns false, then the second one returns true, then overall result will return false which is not what I expected.
I want to return false, if any of the call returns false. How could I able to handle this issue?
func storeInformation(id: String?, _ completion: #escaping (Bool) -> ()) {
guard
let id = id
else {
completion(false)
return
}
let dispatchGroup = DispatchGroup()
var groupResult: Bool = false
dispatchGroup.enter()
storeFeatures { success in
if success {
groupResult = true
} else {
groupResult = false
}
dispatchGroup.leave()
}
dispatchGroup.enter()
storeClasses { success in
if success {
groupResult = true
} else {
groupResult = false
}
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
completion(groupResult)
}
}
private func storeClasses(_ completion: #escaping(Bool) -> Void) {
postClasses { (error) in
if let _ = error {
completion(false)
} else {
completion(true)
}
}
}
private func storeFeatures(_ completion: #escaping(Bool) -> Void) {
postFeatures { (error) in
if let _ = error {
completion(false)
} else {
completion(true)
}
}
}
If we look at your storeClasses and storeFeatures, we see that they are not really actions that return a Bool; they are inherently attempts to post something that can fail. Hence what you really want to know is not whether something returned true or false but whether or not it failed. That is what you really mean — and it is always better, in programming, to say what you mean.
Using the Combine framework, we can express that sort of behavior with unbelievable succinctness. When we have multiple asynchronous actions to perform simultaneously, that is a Merge. And if one of them fails, the entire Merge fails. In other words, the very thing you want to do is effectively automatic!
Imagine, for example, that we have expressed your post actions by wrapping them in deferred Futures of type <Void,Error>. And suppose we have methods storeClassesFuture and storeFeaturesFuture that produce those Futures. Then all you have to say is:
Publishers.Merge(storeClassesFuture(), storeFeaturesFuture())
That is literally all there is to it! If you subscribe to that Merge with a sink, then either it receives a finished completion or a failure completion. And guess what? It receives the failure completion if and only if one or both of the post actions failed! It receives the finished completion only if they both succeeded, which is exactly what you want to know.
As a test bed, here's a sample implementation of your storeInformation (I'm ignoring the String for purposes of the example):
var storage = Set<AnyCancellable>()
enum Oops : Error { case darn }
func storeInformation() {
Publishers.Merge(storeClassesFuture(), storeFeaturesFuture())
.receive(on: DispatchQueue.main)
.sink { (completion) in
switch completion {
case .failure: print("at least one of them failed")
case .finished: print("they both succeeded")
}
print("---")
} receiveValue: { _ in }
.store(in: &storage)
}
And just to act as a random test, here are two futures that can randomly succeed or fail:
func storeClassesFuture() -> AnyPublisher<Void,Error> {
Deferred {
Future<Void,Error> { promise in
if Bool.random() {
print("storeClassesFuture succeeded")
promise(.success(()))
} else {
print("storeClassesFuture failed")
promise(.failure(Oops.darn))
}
}
}.eraseToAnyPublisher()
}
func storeFeaturesFuture() -> AnyPublisher<Void,Error> {
Deferred {
Future<Void,Error> { promise in
if Bool.random() {
print("storeFeaturesFuture succeeded")
promise(.success(()))
} else {
print("storeFeaturesFuture failed")
promise(.failure(Oops.darn))
}
}
}.eraseToAnyPublisher()
}
And here's some sample output from calling storeInformation repeatedly:
storeClassesFuture succeeded
storeFeaturesFuture succeeded
they both succeeded
---
storeClassesFuture failed
storeFeaturesFuture failed
at least one of them failed
---
storeClassesFuture failed
storeFeaturesFuture succeeded
at least one of them failed
---
storeClassesFuture failed
storeFeaturesFuture failed
at least one of them failed
---
storeClassesFuture failed
storeFeaturesFuture succeeded
at least one of them failed
---
storeClassesFuture succeeded
storeFeaturesFuture succeeded
they both succeeded
---
storeClassesFuture succeeded
storeFeaturesFuture succeeded
they both succeeded
---
storeClassesFuture failed
storeFeaturesFuture succeeded
at least one of them failed
---
storeClassesFuture failed
storeFeaturesFuture succeeded
at least one of them failed
---
storeClassesFuture succeeded
storeFeaturesFuture succeeded
they both succeeded
---
As you can see, the logic you're after is perfectly expressed by the Merge of two failable Futures.
(This sort of thing is a very good reason to adopt the Combine framework instead of using DispatchGroup. I find that everything I used to do with DispatchGroup can be done better with Combine. This just happens to be a particularly clearcut instance.)
You have an "AND" semantic here, so you should write that in your code:
let dispatchGroup = DispatchGroup()
var groupResult: Bool = true // identity for AND
dispatchGroup.enter()
storeFeatures { success in
groupResult = groupResult && success // here!
dispatchGroup.leave()
}
dispatchGroup.enter()
storeClasses { success in
groupResult = groupResult && success // and here
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
completion(groupResult)
}
When each task finishes, you want to express the idea that
The group result should be true iff the previous group result is true AND success is true
Related
This question already has answers here:
DispatchGroup logical workflow
(2 answers)
Closed 1 year ago.
I have two methods in the completeOnboarding method and both of them have network operation which should be done in the background thread as follows. However, I am wondering if I am doing why completion(true) gets called first, how could I able to handle that issue?
DispatchQueue.global(qos: .background).async {
self?.completeOnboarding( completion: { (success) in
DispatchQueue.main.async {
if success {
print("success")
} else {
print("failed")
}
}
})
func completeOnboarding(completion: #escaping(Bool) -> Void){
// has network post operation
classRegistration() {(success) in
if !success {
completion(false)
return
}
}
// has network post operation
classLocation() { (success) in
if !success {
completion(false)
return
}
}
completion(true)
}
The final completion(true) is not waiting for classLocation() and classRegistration() calls. If you have multiple network calls and you want to wait for all of them to finish you could (one approach) add them to a DispatchGroup and wait for that one to finish:
func dispatchAndWait(completion: #escaping () -> Void) {
func networkOne(completion: #escaping (_ success: Bool) -> Void) {
print("[DEBUG] Enter \(#function)")
DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in: 0...3)) {
completion(Bool.random())
}
print("[DEBUG] Return \(#function)")
}
func networkTwo(completion: #escaping (_ success: Bool) -> Void) {
print("[DEBUG] Enter \(#function)")
DispatchQueue.main.asyncAfter(deadline: .now() + Double.random(in: 0...3)) {
completion(Bool.random())
}
print("[DEBUG] Return \(#function)")
}
// Create a DispatchGroup and add both calls
let dispatchGroup = DispatchGroup()
// Enter first network call
dispatchGroup.enter()
networkOne { success in
print("[DEBUG] Complete networkOne with success: \(success)")
// Exit first network call
dispatchGroup.leave()
}
// Enter second network call
dispatchGroup.enter()
networkTwo { success in
print("[DEBUG] Complete networkTwo with success: \(success)")
// Exit second network call
dispatchGroup.leave()
}
// notify gets called when all tasks have exited
dispatchGroup.notify(queue: DispatchQueue.main) {
completion()
}
}
One more advise:
classRegistration() {(success) in
if !success {
completion(false)
return
}
}
will never complete in case of success==true, you should make sure that the completion is called on every path
classRegistration() {(success) in
// ... do whatever needs to be done here
completion(success)
}
Assuming classRegistration needs to succeed for classLocation to begin --
quick & dirty --
func completeOnboarding(completion: #escaping(Bool) -> Void){
// has network post operation
classRegistration() {(success) in
if success {
// has network post operation
classLocation() { (success) in
completion(success)
}
} else {
completion(false)
}
}
}
a proper way (others include - NSOperations with dependency, Dispatch group)
func completeOnboarding(completion: #escaping(Bool) -> Void){
let serialQueue = DispatchQueue(label: "classname.serial")
var proceedWithSuccess = true
serialQueue.async {
serialQueue.suspend() //run 1 operation at a time
classRegistration() {(success) in
proceedWithSuccess = success
serialQueue.resume() //let next operation run
}
}
serialQueue.async {
guard proceedWithSuccess else { return }
serialQueue.suspend()
classLocation() { (success) in
proceedWithSuccess = success
serialQueue.resume()
}
}
serialQueue.async {
completion(proceedWithSuccess)
}
}
If you want classLocation() to fire even if registration fails - just get rid of guard statement above.
If it were up to me I'd use a custom NSOperation subclass for Async operation & explicitly mention dependency between operations but it needs a ton of boilerplate code (perhaps something to look into later); serial queue (or dispatch group from the other answer) oughta be enough for you in this case though.
I've been successfully using BrightFutures in my apps mainly for async network requests. I decided it was time to see if I could migrate to Combine. However what I find is that when I combine two Futures using flatMap with two subscribers my second Future code block is executed twice. Here's some example code which will run directly in a playground:
import Combine
import Foundation
extension Publisher {
func showActivityIndicatorWhileWaiting(message: String) -> AnyCancellable {
let cancellable = sink(receiveCompletion: { _ in Swift.print("Hide activity indicator") }, receiveValue: { (_) in })
Swift.print("Busy: \(message)")
return cancellable
}
}
enum ServerErrors: Error {
case authenticationFailed
case noConnection
case timeout
}
func authenticate(username: String, password: String) -> Future<Bool, ServerErrors> {
Future { promise in
print("Calling server to authenticate")
DispatchQueue.main.async {
promise(.success(true))
}
}
}
func downloadUserInfo(username: String) -> Future<String, ServerErrors> {
Future { promise in
print("Downloading user info")
DispatchQueue.main.async {
promise(.success("decoded user data"))
}
}
}
func authenticateAndDownloadUserInfo(username: String, password: String) -> some Publisher {
return authenticate(username: username, password: password).flatMap { (isAuthenticated) -> Future<String, ServerErrors> in
guard isAuthenticated else {
return Future {$0(.failure(.authenticationFailed)) }
}
return downloadUserInfo(username: username)
}
}
let future = authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
let cancellable2 = future.showActivityIndicatorWhileWaiting(message: "Please wait downloading")
let cancellable1 = future.sink(receiveCompletion: { (completion) in
switch completion {
case .finished:
print("Completed without errors.")
case .failure(let error):
print("received error: '\(error)'")
}
}) { (output) in
print("received userInfo: '\(output)'")
}
The code simulates making two network calls and flatmaps them together as a unit which either succeeds or fails.
The resulting output is:
Calling server to authenticate
Busy: Please wait downloading
Downloading user info
Downloading user info <---- unexpected second network call
Hide activity indicator
received userInfo: 'decoded user data'
Completed without errors.
The problem is downloadUserInfo((username:) appears to be called twice. If I only have one subscriber then downloadUserInfo((username:) is only called once. I have an ugly solution that wraps the flatMap in another Future but feel I missing something simple. Any thoughts?
When you create the actual publisher with let future, append the .share operator, so that your two subscribers subscribe to a single split pipeline.
EDIT: As I've said in my comments, I'd make some other changes in your pipeline. Here's a suggested rewrite. Some of these changes are stylistic / cosmetic, as an illustration of how I write Combine code; you can take it or leave it. But other things are pretty much de rigueur. You need Deferred wrappers around your Futures to prevent premature networking (i.e. before the subscription happens). You need to store your pipeline or it will go out of existence before networking can start. I've also substituted a .handleEvents for your second subscriber, though if you use the above solution with .share you can still use a second subscriber if you really want to. This is a complete example; you can just copy and paste it right into a project.
class ViewController: UIViewController {
enum ServerError: Error {
case authenticationFailed
case noConnection
case timeout
}
var storage = Set<AnyCancellable>()
func authenticate(username: String, password: String) -> AnyPublisher<Bool, ServerError> {
Deferred {
Future { promise in
print("Calling server to authenticate")
DispatchQueue.main.async {
promise(.success(true))
}
}
}.eraseToAnyPublisher()
}
func downloadUserInfo(username: String) -> AnyPublisher<String, ServerError> {
Deferred {
Future { promise in
print("Downloading user info")
DispatchQueue.main.async {
promise(.success("decoded user data"))
}
}
}.eraseToAnyPublisher()
}
func authenticateAndDownloadUserInfo(username: String, password: String) -> AnyPublisher<String, ServerError> {
let authenticate = self.authenticate(username: username, password: password)
let pipeline = authenticate.flatMap { isAuthenticated -> AnyPublisher<String, ServerError> in
if isAuthenticated {
return self.downloadUserInfo(username: username)
} else {
return Fail<String, ServerError>(error: .authenticationFailed).eraseToAnyPublisher()
}
}
return pipeline.eraseToAnyPublisher()
}
override func viewDidLoad() {
super.viewDidLoad()
authenticateAndDownloadUserInfo(username: "stack", password: "overflow")
.handleEvents(
receiveSubscription: { _ in print("start the spinner!") },
receiveCompletion: { _ in print("stop the spinner!") }
).sink(receiveCompletion: {
switch $0 {
case .finished:
print("Completed without errors.")
case .failure(let error):
print("received error: '\(error)'")
}
}) {
print("received userInfo: '\($0)'")
}.store(in: &self.storage)
}
}
Output:
start the spinner!
Calling server to authenticate
Downloading user info
received userInfo: 'decoded user data'
stop the spinner!
Completed without errors.
I want to write a test for function that interact with API. I ended up with:
class FileDownloaderTests: XCTestCase {
// MARK: timeouts
let regularTimeout: TimeInterval = 10
let largeTimeout: TimeInterval = 15
func testDownload() {
// URLS.firstFileUrl.rawValue
let downloader = FileDownloader(string: URLS.firstFileUrl.rawValue)
downloader.download(successCompletion: {
XCTAssertTrue(true)
}) { error in
print("error in test - \(error)")
}
waitForExpectations(timeout: largeTimeout, handler: nil)
}
}
So, it suppose to wait largeTimeout(15 seconds) for successCompletion closure, then test should be passed. But it ended up with an error:
*** Assertion failure in -[FileDownloaderTests.FileDownloaderTests waitForExpectationsWithTimeout:handler:], /Library/Caches/com.apple.xbs/Sources/XCTest_Sim/XCTest-14460.20/Sources/XCTestFramework/Async/XCTestCase+AsynchronousTesting.m:28
/Users/Necrosoft/Documents/Programming/Work/Life-Pay/FileDownloader/FileDownloaderTests/FileDownloaderTests.swift:28: error: -[FileDownloaderTests.FileDownloaderTests testDownload] : failed: caught "NSInternalInconsistencyException", "API violation - call made to wait without any expectations having been set."
You need to fulfill the expectation to tell the expectation that it can stop waiting/the process has finished
func testDownload() {
// URLS.firstFileUrl.rawValue
let downloader = FileDownloader(string: URLS.firstFileUrl.rawValue)
downloader.download(successCompletion: {
XCTAssertTrue(true)
expectation.fulfill()
}) { error in
print("error in test - \(error)")
expectation.fulfill()
}
waitForExpectations(timeout: largeTimeout, handler: nil)
}
Note: it is generally not a good idea to run automated tests against a live API. You should either use a stubbed response to just test that your handling of the code is correct or at least test against a test/staging API.
EDIT: you have two completion handlers so I called fulfill in each
use below example to create your own test
func testLogin() throws {
let expectation = XCTestExpectation(description: "DeviceID register with URL")
NetworkAPI.shared.loginRequest(username: "zdravko.zdravkin", password: "password") { authenticated in
switch authenticated {
case true:
XCTAssertTrue(true, "authenticated")
case false:
XCTFail("wrong username, password or deviceID")
}
}
wait(for: [expectation], timeout: 10.0)
}
PromiseKit version: 4.0
Xcode version: 8.3.2
I have recently started using PromiseKit.
Actually, I am creating a polling HTTP request which in returns gives "completed" or "notCompleted".
I have to keep making HTTP calls after every 1 secs for a duration of 5 seconds.
What I need to achieve is if any 1 call gives me completed status I will return fulfilled("completed"). But if all of my requests gives me a response of "notCompleted", I need to return reject("notCompleted")
return Promise<T> { fulfilled, reject
let timer1 = Timer.scheduledTimer(withTimeInterval: TimeInterval(1), repeats: true) { timer in
pArr.append(Promise<T> { f, r in
doSomeAsyncTask { T in
if success {
f(T)
fulfilled(T)
timer.invalidate()
} else {
r(ErrorNotCompleted)
}
}
// timeout option
_ = after(interval: TimeInterval(15)).then(execute: { () -> Void in
reject(timeoutForConfirmation)
})
})
}
Timer.scheduledTimer(withTimeInterval: TimeInterval(5), repeats: false) { timer in
timer1.invalidate()
timer.invalidate()
when(resolved: pArr).then { results in
let count = results.filter({ result -> Bool in
return result.boolValue
}).count
if count == 0 {
// TODO: then reject here
reject(ErrorNotCompleted)
}
}.catch { error in
print(error)
}
}
}
timer1.fire()
How can I achieve this?
Is there a better way to write the above code in PromiseKit.
Here is a way to do a basic loop with Promises... It's from my gist: https://gist.github.com/dtartaglia/2b19e59beaf480535596
I think all you would need to do is make sure your promise producer (body) has a suitable delay in it before making the network call.
/**
Repeadetly evaluates a promise producer until a value satisfies the predicate.
`promiseWhile` produces a promise with the supplied `producer` and then waits
for it to resolve. If the resolved value satifies the predicate then the
returned promise will fulfill. Otherwise, it will produce a new promise. The
method continues to do this until the predicate is satisfied or an error occurs.
- Returns: A promise that is guaranteed to fulfill with a value that satisfies
the predicate, or reject.
*/
func promiseWhile<T>(pred: (T) -> Bool, body: () -> Promise<T>, fail: (() -> Promise<Void>)? = nil) -> Promise<T> {
return Promise { fulfill, reject in
func loop() {
body().then { (t) -> Void in
if !pred(t) { fulfill(t) }
else {
if let fail = fail {
fail().then { loop() }
.error { reject($0) }
}
else { loop() }
}
}
.error { reject($0) }
}
loop()
}
}
In my example code below, I call complete(false) on failure. However, since I'm using a DispatchGroup object to make sure all asynchronous requests are complete, I cannot just call syncGroup.leave() on failure, as the notify will be called, which contains complete(true), making this function return true, when it should be returning false for failure.
Am I correct in not calling syncGroup.leave() on failure to complete my function correctly? Or should I be calling syncGroup.leave() and somehow trying to determine what the result is, so I can return false on failure?
let syncGroup = DispatchGroup()
syncGroup.enter()
for track in unsynced {
register(time: time, withCompletion: { (success: Bool) -> () in
if success {
self.debug.log(tag: "SyncController", content: "Registered")
syncGroup.leave()
}
else {
complete(false)
}
})
}
//all requests complete
syncGroup.notify(queue: .main) {
self.debug.log(tag: "SyncController", content: "Finished registering")
complete(true)
}
You have to enter the group within your for loop. You might want to introduce an additional error flag.
Example implementation:
var fail = false
let syncGroup = DispatchGroup()
for track in unsynced {
syncGroup.enter()
register(time: time, withCompletion: { (success: Bool) -> () in
if success {
self.debug.log(tag: "SyncController", content: "Registered")
syncGroup.leave()
}
else {
fail = true
syncGroup.leave()
}
})
}
//all requests complete
syncGroup.notify(queue: .main) {
if fail {
complete(false)
} else {
self.debug.log(tag: "SyncController", content: "Finished registering")
complete(true)
}
}