Promise + progress + pause + cancel + retry for Swift.

Related tags

EventBus SwiftTask
Overview

SwiftTask Circle CI

Promise + progress + pause + cancel + retry for Swift.

SwiftTask

How to install

See ReactKit Wiki page.

Example

Basic

// define task
let task = Task<Float, String, NSError> { progress, fulfill, reject, configure in

    player.doSomethingWithProgress({ (progressValue: Float) in
        progress(progressValue) // optional
    }, completion: { (value: NSData?, error: NSError?) in
        if error == nil {
            fulfill("OK")
        }
        else {
            reject(error)
        }
    })

    // pause/resume/cancel configuration (optional)
    configure.pause = { [weak player] in
        player?.pause()
    }
    configure.resume = { [weak player] in
        player?.resume()
    }
    configure.cancel = { [weak player] in
        player?.cancel()
    }

}

// set success & failure
task.success { (value: String) -> Void in
    // do something with fulfilled value
}.failure { (error: NSError?, isCancelled: Bool) -> Void in
    // do something with rejected error
}

// you can call configured operations outside of Task-definition
task.pause()
task.resume()
task.cancel()

Notice that player has following methods, which will work nicely with SwiftTask:

  • doSomethingWithProgress(_:completion:) (progress callback as optional)
  • pause() (optional)
  • resume() (optional)
  • cancel() (optional)

One of the best example would be Alamofire (networking library) as seen below.

Using Alamofire

typealias Progress = (bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
typealias AlamoFireTask = Task<Progress, String, NSError>

// define task
let task = AlamoFireTask { progress, fulfill, reject, configure in

    Alamofire.download(.GET, "http://httpbin.org/stream/100", destination: somewhere)
    .progress { bytesWritten, totalBytesWritten, totalBytesExpectedToWrite in

        progress((bytesWritten, totalBytesWritten, totalBytesExpectedToWrite) as Progress)

    }.response { request, response, data, error in

        if let error = error {
            reject(error)
            return
        }

        fulfill("OK")

    }

    return
}

// set progress & then
task.progress { (oldProgress: Progress?, newProgress: Progress) in

    println("\(newProgress.bytesWritten)")
    println("\(newProgress.totalBytesWritten)")
    println("\(newProgress.totalBytesExpectedToWrite)")

}.then { (value: String?, errorInfo: AlamoFireTask.ErrorInfo?) -> Void in
    // do something with fulfilled value or rejected errorInfo
}

Retry-able

Task can retry for multiple times by using retry() method. For example, task.retry(n) will retry at most n times (total tries = n+1) if task keeps rejected, and task.retry(0) is obviously same as task itself having no retries.

This feature is extremely useful for unstable tasks e.g. network connection. By implementing retryable from SwiftTask's side, similar code is no longer needed for player (inner logic) class.

task.retry(2).progress { ... }.success { ...
    // this closure will be called even when task is rejected for 1st & 2nd try
    // but finally fulfilled in 3rd try.
}

For more examples, please see XCTest cases.

API Reference

Task.init(initClosure:)

Define your task inside initClosure.

let task = Task<Float, NSString?, NSError> { progress, fulfill, reject, configure in

    player.doSomethingWithCompletion { (value: NSString?, error: NSError?) in
        if error == nil {
            fulfill(value)
        }
        else {
            reject(error)
        }
    }
}

In order to pipeline future task.value or task.errorInfo (tuple of (error: Error?, isCancelled: Bool)) via then()/success()/failure(), you have to call fulfill(value) and/or reject(error) inside initClosure.

Optionally, you can call progress(progressValue) multiple times before calling fulfill/reject to transfer progressValue outside of the initClosure, notifying it to task itself.

To add pause/resume/cancel functionality to your task, use configure to wrap up the original one.

// NOTE: use weak to let task NOT CAPTURE player via configure
configure.pause = { [weak player] in
    player?.pause()
}
configure.resume = { [weak player] in
    player?.resume()
}
configure.cancel = { [weak player] in
    player?.cancel()
}

task.progress(_ progressClosure:) -> task

task.progress { (oldProgress: Progress?, newProgress: Progress) in
    println(newProgress)
    return
}.success { ... }

task.progress(progressClosure) will add progressClosure to observe old/new progressValue which is notified from inside previous initClosure. This method will return same task, so it is useful to chain with forthcoming then/success/failure.

task.then(_ thenClosure:) -> newTask

task.then(thenClosure) will return a new task where thenClosure will be invoked when task is either fulfilled or rejected.

This case is similar to JavaScript's promise.then(onFulfilled, onRejected).

thenClosure can be two types of closure form:

  1. thenClosure: (Value?, ErrorInfo?) -> Value2 (flow: task => newTask)
// let task will be fulfilled with value "Hello"

task.then { (value: String?, errorInfo: ErrorInfo?) -> String in
    // nil-check to find out whether task is fulfilled or rejected
    if errorInfo == nil {
        return "\(value!) World"
    }
    else {
        return "\(value!) Error"
    }
}.success { (value: String) -> Void in
    println("\(value)")  // Hello World
    return"
}
  1. thenClosure: (Value?, ErrorInfo?) -> Task (flow: task => task2 => newTask)
// let task will be fulfilled with value "Hello"

task.then { (value: String?, errorInfo: ErrorInfo?) -> Task<Float, String, NSError> in
    if errorInfo == nil {
        // let task2 will be fulfilled with value "\(value!) Swift"
        let task2 = ...
        return task2
    }
    else {
        return someOtherTask
    }
}.success { (value: String) -> Void in
    println("\(value)")  // Hello Swift
    return"
}

task.success(_ successClosure:) -> newTask

Similar to then() method, task.success(successClosure) will return a new task, but this time, successClosure will be invoked when task is only fulfilled.

This case is similar to JavaScript's promise.then(onFulfilled).

// let task will be fulfilled with value "Hello"

task.success { (value: String) -> String in
  return "\(value) World"
}.success { (value: String) -> Void in
  println("\(value)")  // Hello World
  return"
}

task.failure(_ failureClosure:) -> newTask

Just the opposite of success(), task.failure(failureClosure) will return a new task where failureClosure will be invoked when task is only rejected/cancelled.

This case is similar to JavaScript's promise.then(undefined, onRejected) or promise.catch(onRejected).

// let task will be rejected with error "Oh My God"

task.success { (value: String) -> Void in
    println("\(value)") // never reaches here
    return
}.failure { (error: NSError?, isCancelled: Bool) -> Void in
    println("\(error!)")  // Oh My God
    return
}

task.try(_ tryCount:) -> newTask

See Retry-able section.

Task.all(_ tasks:) -> newTask

Task.all(tasks) is a new task that performs all tasks simultaneously and will be:

  • fulfilled when all tasks are fulfilled
  • rejected when any of the task is rejected

Task.any(_ tasks:) -> newTask

Task.any(tasks) is an opposite of Task.all(tasks) which will be:

  • fulfilled when any of the task is fulfilled
  • rejected when all tasks are rejected

Task.some(_ tasks:) -> newTask

Task.some(tasks) is a new task that performs all tasks without internal rejection, and is fulfilled with given tasks's fulfilled values. Note that this new task will be fulfilled with empty value-array, even though all tasks are rejected.

Related Articles

Licence

MIT

Comments
  • Add `on(success:failure:)` for adding side-effects.

    Add `on(success:failure:)` for adding side-effects.

    This is a new feature and improvement on #50 to add side-effects after completion (success or failure) of task.

    Please see SwiftTaskTests.swift#L425-L474 for more detail.

    Note

    Using Self for returning value will fail in Xcode 7.0, but not in 7.1.

    enhancement 
    opened by inamiy 9
  • implement zip function

    implement zip function

    would be great to have a zip function that takes two tasks with different progress, value and error types and returns a task that returns a tuple of values.

    I've implemented the function with the signature

    public class func zip<P1, V1, R1, P2, V2, R2>(task1: Task<P1, V1, R1>, _ task2: Task<P2, V2, R2>) -> Task<(P1, P2), (V1, V2), (R1?, R2?)>
    

    but unfortunately I'm unable to call it without the error:

    Cannot invoke 'zip' with an argument list of type '(Task<Void, String, String>, Task<Void, String, String>)'
    
    opened by DanielAsher 6
  • Failure return `value`

    Failure return `value`

    Maybe I'm completely confused about the way this works, but shouldn't a failureClosure return Void? Currently, it's trying to return the Value type.

    The README shows failure returning Void, which makes sense. Why is the failureClosure returning the value? If the task has failed, shouldn't there be no value?

    I'm unable to use this, because, for the task I'm trying to create, if the task has failed, I can't return a value, since the value doesn't exist.

    opened by AnthonyMDev 6
  • Add retry(_:condition:) method.

    Add retry(_:condition:) method.

    Hi, I want conditional retryable feature. In my use case, I want retry failed task with date time predication like this.

    task.retry(5) { () -> Bool in
      let now = NSDate()
      return now.compare(expireDate) == .OrderedAscending
    }.success { (value) -> Void in
     ...
    

    What do you think about this?

    opened by r-plus 5
  • Add retryable feature.

    Add retryable feature.

    This pull request brings a new retryable feature for future ver 2.1.0.

    task.try(n) returns a new task that is retryable for n-1 times, and is conceptually equal to

    task.failure(clonedTask1).failure(clonedTask2)...
    

    with n-1 failure-able.

    Usage

    task.try(3).success { ...
        // this closure will be called even when task is rejected for 1st & 2nd try 
        // but fulfilled in 3rd try.
    }
    
    // shorthand
    (task ~ 3).then { ... }
    
    opened by inamiy 5
  • Progress propagation

    Progress propagation

    I am having problems getting progress events to propagate up the chain of tasks.

    I have a lower level String task that will generate progress. Then, upon success I want to convert that String value to in Int if possible or an error otherwise. I expect progress handlers on both tasks to be called whenever progress changes on the lower level task. Is this a valid expectation?

    To be clear, consider the following test (using Nimble) that always fails where indicated below:

    class ProgressTest: QuickSpec {
        override func spec() {
            var stringTask: Task<Float, String, NSError>!
            var stringTaskProgressHandler: (Float ->Void)?
            var stringTaskProgressValue: Float?
    
            var intTask: Task<Float, Int, NSError>!
            var intTaskProgressValue: Float?
    
            beforeEach {
                stringTask = Task<Float, String, NSError> { progress, fulfill, reject, configure in
                    stringTaskProgressHandler = progress
                }.progress { oldValue, newValue in
                    stringTaskProgressValue = newValue
                }
    
                intTask = stringTask.success { stringValue -> Task<Float, Int, NSError> in
                    if let intValue = Int(stringValue) {
                        return Task(value: intValue)
                    } else {
                        return Task(error: NSError(domain: "test", code: 123, userInfo: nil))
                    }
                }.progress { oldValue, newValue in
                    intTaskProgressValue = newValue
                }
            }
    
            describe("when string task has progress") {
                beforeEach {
                    stringTaskProgressHandler?(0.5)
                }
    
                it("string task progress should fire") {
                    expect(stringTaskProgressValue).toEventually(equal(0.5))
                }
                it("should propagate progress to intTask") {
                    // ************ This test fails! ************
                    expect(intTaskProgressValue).toEventually(equal(0.5))
                }
            }
        }
    }
    

    Thoughts?

    opened by mjbeauregard 4
  • Failure in Task.all() make recursive calls

    Failure in Task.all() make recursive calls

    When calling Task.all() with many tasks and one of the task was failed, canceling all tasks process will start. But from the current algorithm, it makes recursive call. If the number of tasks is 10000 and the first task is failed, the depth of call is about 10000 and it causes stack overflow. So, the proposal is that prepare flag for limiting the depth of call to be 1.

                }.failure { (errorInfo: ErrorInfo) -> Void in
    
                        lock.lock()
                        _reject(errorInfo)
    
                        for task in tasks {
                            task.cancel()  // <- this call invokes ".failure" of another task in the same context
                        }
                        lock.unlock()
                }
    
    opened by aKentaroIchima 3
  • Create tasks manager to retain tasks

    Create tasks manager to retain tasks

    Is it possible to retain task while it is executed in one scope? I have found that tasks deinit after creating )) even they are returned from success block of other task. Probably we need something that will retain task while it is executing like in next example:

    public class Task<Progress, Value, Error> {
    
        //...
    
        class func createTask<Progress, Value, Error>(initClosure: InitClosure) -> Task<Progesss, Value, Error> {
            // Paused
            let task = Task<Progesss, Value, Error>(paused: true, initClosure: initClosure)
            task.onFinish = {
                TasksRegistry.unregister(task) // Release
            }
            TasksRegistry.register(task) // Retain
            return task
        }
    
        //...
    }
    
    

    Or is there any other ways to do so?

    opened by Koshub 3
  • chain Task.all more than once, last `then` not called.

    chain Task.all more than once, last `then` not called.

    Under the following conditions, last then not called.

    • Task.all chain more than once
    • all tasks is empty

    what would be a good way to do it?

    Sample code

    final class Image {
      let url: String
    
      var downloaded: Bool = false
      var converted: Bool = false
    
      typealias DownloadTask = Task<NSProgress, String, NSError?>
      typealias ConvertTask = Task<NSProgress, String, NSError?>
    
      init(url: String) {
        self.url = url
      }
    
      func download() -> DownloadTask {
        return DownloadTask { progress, fulfill, reject, configure in
          // download image and cache
          self.downloaded = true
          return fulfill("path to image")
        }
      }
    
      func convert() -> ConvertTask {
        return ConvertTask { progress, fulfill, reject, configure in
          // convert image
          self.converted = true
          return fulfill("path to converted image")
        }
      }
    }
    
    func downloadAndConvertAll() {
      let images = [
        Image(url: "http://example.com/1.jpg"),
        Image(url: "http://example.com/2.jpg"),
        Image(url: "http://example.com/3.jpg"),
      ]
    
      // I want to exclude downloaded image
      let downloadTasks = images.filter { !$0.downloaded }.map { $0.download() }
    
      // downloadTasks.count == 0 and convertTasks.count == 0 => doSomething not called
      Task.all(downloadTasks).then { _ -> Task<Image.ConvertTask.BulkProgress, [String], NSError?> in
        // I want to exclude converted image
        let convertTasks = images.filter { !$0.converted }.map { $0.convert() }
        return Task.all(convertTasks)
      }.then { _ in
        doSomething()
      }
    }
    
    func doSomething() {
      print("doSomething")
    }
    

    found workaround

    Task.all(downloadTasks.count > 0 ? downloadTasks : [Image.DownloadTask(value: "dummy")])
    

    and

    Task.all(convertTasks.count > 0 ? convertTasks : [Image.ConvertTask(value: "dummy")])
    
    opened by taka0125 3
  • add .success / .failure methods that cause sideeffects only

    add .success / .failure methods that cause sideeffects only

    It will be handy if we could just add event handlers without changing values or types of Tasks (now SwiftTask requires .success / .failure closures to return a new value or Task). In particular, it's painful to use .failure when you just want to add an event handler and don't want to recover from failures, because it still requires successful return value.

    This PR adds .success / .failure methods which have closures of returning type Void for their arguments. You pass a closure returns nothing (or Void), and you get a new Task nearly identical to previous one but it keeps the closure as an event handler.

    This change disables handling Tasks with Void as the Value type (eg. Task<Progress, Void, Error>), but no one might want to use such creepy Tasks :wink:

    enhancement 
    opened by tamamachi 3
  • Handling multiple error types.

    Handling multiple error types.

    This pull request will fix https://github.com/ReactKit/SwiftTask/issues/37 by:

    • [x] improving type-inference for overloaded then()/success()/failure() to not use the wrong one
    • [x] using then() to promote Task type from Task<P1, V1, E1> to Task<P2, V2, E2> (including E1 -> E2 Error type change)

    For more information, please see MultipleErrorTypesTests.swift.

    Caveat

    let task1: Task1 = self.request1()
    task1.success { _ -> Task2 in
        return self.request2() // let's say, this task will fail
    }.then { value, errorInfo -> Void in
        // NOTE:
        // Though `errorInfo` will still be non-nil, `errorInfo!.error` will become as `nil`
        // if Task1 and Task2 have different Error types.
    }
    

    If Task1 and Task2 have different Error types and request2 fails, you will still receive errorInfo as non-nil, but now errorInfo!.error will become as nil.

    This is because there is no way to convert request2's error back to task1's Error type (which is required in success() task-flow).

    To avoid this default behavior, it is user's job to add error-conversion logic to maintain same Error type throughout the task-flow, by adding another type-promotable then()/success()/failure().

    bug 
    opened by inamiy 3
  • Publicly visible Swift 3 Support

    Publicly visible Swift 3 Support

    I see that there is a branch for Swift 3 support open. What is the roadmap for having the Swift 3 branch merged and available publicly via cocoapods?

    (Specifically, looking to consume the library in a podspec, where the branch cannot be specified)

    opened by stephengazzard 3
  • Some strange behaviour

    Some strange behaviour

    Can you help please? Found some case when failure block is called after task is fulfilled. I don't understand how could it be possible but from stacktrace of XCode cancel method is called from task deinit. This cause the task to be handled as canceled in my code, but I has fulfilled it. I don't understand what I made wrong.

    screen shot 2016-10-03 at 10 16 08 pm
    opened by Koshub 0
  • Implement when function

    Implement when function

    I implement "when" method. It corresponds to multiple value type.

    Usage

    • success pattern
            let task1 = Task<Float, String, NSError?> { fulfill, reject in fulfill("Success") }
            let task2 = Task<Float, Int, NSError?> { fulfill, reject in fulfill(1) }
            let task3 = Task<Float, Double, NSError?> { fulfill, reject in fulfill(1.1) }
    
            Task<Float, (String, Int, Double), NSError>.when((task1, task2, task3)).success { (string, int, double) -> Void in
                print(string) // -> Success
                print(int) // -> 1
                print(double) // -> 1.1
            }.failure { (error, isCancelled) -> Void in
                fatalError()
            }
    

    *failure pattern

            let task1 = Task<Float, String, String> { fulfill, reject in fulfill("Success") }
            let task2 = Task<Float, Int, String> { fulfill, reject in fulfill(1) }
            let task3 = Task<Float, Double, String> { fulfill, reject in  reject("Rejected") }
    
            Task<Float, (String, Int, Double), String>.when((task1, task2, task3)).success { (string, int, double) -> Void in
                fatalError()
            }.failure { (error, isCancelled) -> Void in
                print(error) // -> Optional("Rejected")
            }
    
    opened by bannzai 1
Releases(5.0.0)
Owner
ReactKit
Swift Reactive Programming Toolkit
ReactKit
A Promise library for Swift, based partially on Javascript's A+ spec

Promise A Promise library for Swift, based partially on Javascript's A+ spec. What is a Promise? A Promise is a way to represent a value that will exi

Soroush Khanlou 622 Nov 23, 2022
A light-weighted Promise library for Objective-C

RWPromiseKit Desiciption A light-weighted Promise library for Objective-C About Promise The Promise object is used for deferred and asynchronous compu

Canopus 113 May 4, 2022
Promises for Swift & ObjC.

Promises simplify asynchronous programming, freeing you up to focus on the more important things. They are easy to learn, easy to master and result in

Max Howell 14k Jan 5, 2023
When is a lightweight implementation of Promises in Swift.

Description When is a lightweight implementation of Promises in Swift. It doesn't include any helper functions for iOS and OSX and it's intentional, t

Vadym Markov 260 Oct 12, 2022
FutureLib is a pure Swift 2 library implementing Futures & Promises inspired by Scala.

FutureLib FutureLib is a pure Swift 2 library implementing Futures & Promises inspired by Scala, Promises/A+ and a cancellation concept with Cancellat

Andreas Grosam 39 Jun 3, 2021
A Swift based Future/Promises Library for IOS and OS X.

FutureKit for Swift A Swift based Future/Promises Library for IOS and OS X. Note - The latest FutureKit is works 3.0 For Swift 2.x compatibility use v

null 759 Dec 2, 2022
Write great asynchronous code in Swift using futures and promises

BrightFutures How do you leverage the power of Swift to write great asynchronous code? BrightFutures is our answer. BrightFutures implements proven fu

Thomas Visser 1.9k Dec 20, 2022
⚡️ Lightweight full-featured Promises, Async & Await Library in Swift

Lightweight full-featured Promises, Async & Await Library in Swift What's this? Hydra is full-featured lightweight library which allows you to write b

Daniele Margutti 2k Dec 31, 2022
The easiest Future and Promises framework in Swift. No magic. No boilerplate.

Promis The easiest Future and Promises framework in Swift. No magic. No boilerplate. Overview While starting from the Objective-C implementation of Ju

Alberto De Bortoli 111 Dec 27, 2022
Promises is a modern framework that provides a synchronization construct for Swift and Objective-C.

Promises Promises is a modern framework that provides a synchronization construct for Objective-C and Swift to facilitate writing asynchronous code. I

Google 3.7k Dec 24, 2022
Easy Swift Futures & Promises.

❗️ Archived now ❗️ Since Apple released Combine framework, I decide to archive this repo. You still can use this repo as an example of Future/Promise

Dmytro Mishchenko 40 Sep 23, 2022
Material para a apresentação da palestra "Implementando Interesses Transversais - um papo sobre arquitetura, DI e Design Patterns em Swift/iOS" no TDC Future 2021

--- title: Implementando Interesses Transversais - um papo sobre arquitetura, DI e Design Patterns em Swift/iOS author: Cícero Camargo date: Nov 30th

Cícero Camargo 2 Nov 30, 2021
PromiseKit 4.5.2 with changes for Swift 5

繁體中文, 简体中文 Promises simplify asynchronous programming, freeing you up to focus on the more important things. They are easy to learn, easy to master an

null 0 Dec 24, 2021
A promises library written in Swift featuring combinators like map, flatMap, whenAll, whenAny.

Promissum is a promises library written in Swift. It features some known functions from Functional Programming like, map and flatMap. It has useful co

Tom Lokhorst 68 Aug 31, 2022
Swift µframework providing Future

Future [] (https://github.com/Carthage/Carthage) Swift µframework providing Future<T, Error>. This library is inspired by the talk of Javier Soto at S

Le Van Nghia 122 Jun 3, 2021
A library that adds a throwing unwrap operator in Swift.

ThrowingUnwrap A simple package to add a throwing unwrap operator (~!) to Optionals in Swift. Import Add this to the package-wide dependencies in Pack

Allotrope 3 Aug 27, 2022
Nice category that adds the ability to set the retry interval, retry count and progressiveness.

If a request timed out, you usually have to call that request again by yourself. AFNetworking+RetryPolicy is an objective-c category that adds the abi

Jakub Truhlář 209 Dec 2, 2022
Animated Play and Pause Button written in Swift, using CALayer, CAKeyframeAnimation.

AnimatablePlayButton Animated Play and Pause Button written in Swift, using CALayer, CAKeyframeAnimation. features Only using CAShapeLayer, CAKeyframe

keishi suzuki 77 Jun 10, 2021
Job Scheduler for IOS with Concurrent run, failure/retry, persistence, repeat, delay and more

SwiftQueue Schedule tasks with constraints made easy. SwiftQueue is a job scheduler for iOS inspired by popular android libraries like android-priorit

Lucas Nelaupe 367 Dec 24, 2022
pick the voice from the local storage.you can play and pause the voice

flutter_add_voice A new Flutter project. Getting Started This project is a starting point for a Flutter application. A few resources to get you starte

Mehrab Bozorgi 1 Nov 27, 2021