belozierov / SwiftCoroutine

Swift coroutines for iOS, macOS and Linux.
https://belozierov.github.io/SwiftCoroutine
MIT License
836 stars 51 forks source link

Add finally block feature #25

Closed mickaelhero closed 4 years ago

mickaelhero commented 4 years ago

Hi,

I developed a feature that I found missing.

The problem is that I want to continue the execution of a coroutine block even if there is an error, for example a loader must disappear after the request whether there is an error or not.

With current framework:

DispatchQueue.main.startCoroutine(in: self.scope) {

isLoading = true 

    do {
        try self.service.getAll().await()
        try self.service.getAll().await() // Error occurred go to catch block
        try self.service.getAll().await()

        isLoading = false
    } catch {
        // Error occurred
         isLoading = false
    }
}

With this version as soon as there is an error we do not continue the block we go into the catch block. But the problem is that isLoading = false is written twice

I know I can also do this which will not stop the block but it is not what I reach, if there is an error I want the block to be stopped

DispatchQueue.main.startCoroutine(in: self.scope) {

isLoading = true 

        try? self.service.getAll().await()
        try? self.service.getAll().await() // Error occurred but I dont want continue to the next request, I want hide the loader
        try? self.service.getAll().await()

        isLoading = false
}

So my solution is as follows, I wrap the coroutine block to make it clearer, Async / Await. And I added 2 blocks to catch the error, and a finally block which will be executed even if we get an error.

isLoading = true

async {
    try self.service.getAll().await()
    try self.service.getAll().await() // Error go to catch
    try self.service.getAll().await()
}.catch { error in
    Log.s("Error occurred:  \(error.localizedDescription)")
}.finally {
   // This block will be executed at the end of the block even if there is an error
   isLoading = false
 }

I already wrote this wrapper, if you are interested in integrating it into the framework I can make a pull request. This way will not replace the old way, it will just be a new possibility way

belozierov commented 4 years ago

@mickaelhero Hi, I like your approach but the main idea of this framework is to reduce the use of chains and other reactive programming approaches (although the framework includes everything you need to build chains, but it's like extra features).

The native Swift version should look like this:

DispatchQueue.main.startCoroutine {
   isLoading = true

   do {
      try self.service.getAll().await()
   } catch {
      Log.s("Error 1 occurred:  \(error.localizedDescription)")
   }

   do {
      try self.service.getAll().await()
   } catch {
      Log.s("Error 2 occurred:  \(error.localizedDescription)")
   }

   isLoading = false
}

You can do it a little better with extensions:

extension CoFuture where Value == Void {

   func await(catchBlock: (Error) -> Void) {
      do { try await() } catch { catchBlock(error) }
   }

}

DispatchQueue.main.startCoroutine {
   var isLoading = true

   self.service.getAll().await { error in
      Log.s("Error 1 occurred:  \(error.localizedDescription)")
   }

   self.service.getAll().await { error in
      Log.s("Error 2 occurred:  \(error.localizedDescription)")
   }

   isLoading = false
}

You can also create an extension for Sequence where Element: CoFuture<Void>, etc.

belozierov commented 4 years ago

@mickaelhero You can also do so, it all depends on what errors you want to handle and how:

DispatchQueue.main.startCoroutine {
   isLoading = true
   defer { isLoading = false }

   do {
      try self.service.getAll().await()
      try self.service.getAll().await()
      try self.service.getAll().await()
   } catch {
      Log.s("Error occurred:  \(error.localizedDescription)")
   }
   // Or you can put `isLoading = false` here if you don’t like `defer`
}
belozierov commented 4 years ago

@mickaelhero Also, you can use the available methods to build chains:

DispatchQueue.main.coroutineFuture {
   try self.service.getAll().await()
   try self.service.getAll().await()
   try self.service.getAll().await()
}.recover { error in
   Log.s("Error occurred:  \(error.localizedDescription)")
}.whenComplete {
   isLoading = false
}

But I'm not a fan of chains for a number of reasons. The idea of the async/await pattern is to reduce or avoid their use. The do-catch version is more readable, native and doesn't require additional allocation of CoFuture or any other objects.

Also, as an option you can write, quite succinctly:

DispatchQueue.main.coroutineFuture {
   isLoading = true
   defer { isLoading = false }

   try self.service.getAll().await()
   try self.service.getAll().await()
   try self.service.getAll().await()
}.whenFailure { error in
   Log.s("Error occurred:  \(error.localizedDescription)")
}
mickaelhero commented 4 years ago

Thanks for your answer helped me a lot :). The best async/await lib!