Latest 0.1.1
Homepage https://github.com/knguyen2708/Firefly
License MIT
Platforms ios 8.0
Authors

Firefly

Async that makes sense.

Carthage compatible
CocoaPods Compatible

Installation

Carthage

Add the framework to your Cartfile

github "knguyen2708/Firefly"

Run carthage update to checkout and build the framework.

In your project’s main target, select General tab, scroll down to Embedded Binaries section, and drag the framework there.

Cocoa Pods

Add the framework to your Podfile

source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '10.0'
use_frameworks!

target 'FireflyCocoaPodsTest' do
pod 'Firefly', '~> 0.1.1'
end

Run pod update.

Manually

  1. Download the source code
  2. Open Firefly/Firefly/Firefly.xcodeproject
  3. Build the Firefly target with release configuration
  4. Under Products group, right click Firefly.framework, select "Show in Finder"
  5. Copy the framework to somewhere under your project’s folder.
  6. In your project’s main target, select General tab, scroll down to Embedded Binaries section, and drag the framework there.
  7. All good, see FireflySamples project for how this was done, and for some examples.

Creating tasks

Firefly revolves around the concept of task, which simply is a chunk of code that can be executed when needed.

A task signals when it completes, either with success (with a result) or failure (with an error), can report its progress and can be cancelled.

Creating a task:

import Firefly

// Int is result type and Error is error type
let task: Task<Int, Error> = Firefly.task { (succeed, fail) in
    /* Do some work */
    // When the work finishes, call either `succeed` with a result 
    // or `fail` with an error
}

Creating a task does not execute it.

A task created on a thread will be executed on the same thread, unless you explicitly change that in your task’s execution. Firefly does not interfere with your threading model.

Since you probably don’t want a task to be executed on the main thread most of the time (who needs this library otherwise), your code will probably look like this

// Calculating n-th fibonacci number on a global queue and returns the result on main queue
// n is a positive integer
// fibonacci(n:) is a function that returns n-th Fibonacci number

Firefly.task { (succeed, fail, progress) in        
    // Execute on global queue
    DispatchQueue.global().async {
        let f = fibonacci(n: n)

        // Complete on main queue
        DispatchQueue.main.async {
            succeed(f)
        }
    }
}

Or when networking is involved

// Downloading file with Alamofire
Firefly.task { (succeed, fail, progress) in
    let destination = /* prepare download destination */

    Alamofire.download(url, to: destination)
        .downloadProgress(queue: DispatchQueue.main) { p in                
             progress(TaskProgress(p.fractionCompleted)) // Progress callback
        }
        .responseData { response in
            switch response.result {
            case .success(let data):
                succeed(data) // Success callback
            case .failure(let error):
                fail(error) // Failure callback
            }
        }            
}

Note that TaskProgress is used here to report the download progress. You can attach arbitrary data to TaskProgress in addition to completed fraction.

Execution and cancellation

Executing a task

Executing a task:

task.execute()

You can register to success and failure of the task using success and failure methods before executing it.

task
    .success { result in
        print("Result: (result)")      
    }
    .failure { error in
        print("failed: (error)")
    }
    .execute()

Do not forget to call execute or you’ll be left scratching your head why nothing is happening!

Success and failure are unified via TaskOutcome type, and there is completion method that gets called whether the task succeeds or fails.

task.completion { outcome in
        switch outcome {
            case .success(let result): /* handle success */
            case .failure(let error): /* handle failure */
        }
    }

You can listen to progress updates using progress method.

task.progress { progress in
        print("Completed (progress.fraction)")
    }

Task states

Tasks are always in one of four states:

  • Not executed
  • Executing
  • Completed
  • Cancelled

State of a task can be queried via state property.

Cancelling a task

A task can be cancelled by calling cancel method. Cancelling a task stops its success and failure methods from being called, and if the task is comprised of subtasks, the following rules are followed:

  • Completed and cancelled subtasks are left alone
  • Executing subtasks are cancelled
  • Not yet executed subtasks are left alone, and will never be executed

Coordinating multiple tasks

Firefly offers 3 different ways to coordinate task executions.

Chaining

then methods are used to chain tasks upon success.

// When task succeeds, `otherTask` is executed.
task.then(otherTask)

// `otherTask` can also be computed from the result
task.then { result in
    let otherTask = /* computed from `result` */
    return otherTask
}

recover methods are used to chain upon failure.

// When `task` fails, `otherTask` is executed.
task.recover(otherTask)

// `otherTask` can also be computed from the error
task.recover { error in
    let otherTask = /* computed from `error` */
    return otherTask
}

There is also chain method which is a generalization of then and recover.

task.chain { outcome in 
    let otherTask = /* computed from `outcome` */
    return otherTask
}

Sequential execution

If you know in advance the tasks that needs to be executed, it is better to use sequential method, which takes an array of subtasks and executes them sequentially.

Firefly.sequential([subtask1, subtask2, subtask3, subtask4])

If one subtask fails, the remaining ones are never executed and the task immediately fails with the error the subtask failed with.

Unlike chaining, sequential knows all the subtasks in advance, and thus takes care of calculating aggregated progress. For example

Firefly.sequential([subtask1, subtask2, subtask3, subtask4])
    .progress { progress in
        print("(progress.fraction)")
    }
    .execute() // Again, don't forget this!

will output 0.25 when subtask1 succeeds, 0.50 when subtask2 succeeds, so on.

You can also set a weight for each subtask via weight propery or setWeight method. Subtasks with more weight adds more to the aggregated progress.

Concurrent execution

concurrent creates a task that executes its subtasks in parallel, succeeds when all subtasks complete, and fails as soon as any of them fails.

Firefly.concurrent([task1, task2, task3, task4])

Aggregated progress are calculated similar to sequential.

Other stuffs

Ways to create a task

Firefly.task(_ closure: (succeed, fail))

Creates a closure-based task.

Firefly.task(_ closure: (succeed, fail, progress))

Creates a closure-based task. The closure can report progress.

Firefly.task<R, E>(_ closure: (task, succeed, fail, progress))

Creates a closure-based task. The closure can report progress and has access to the task itself, presumably to stop a long-running operation if the task is cancelled midway.

Firefly.succeed<R>(_ result: R)

Creates a task that immediately succeeds.

Firefly.fail<E>(_ error: E)

Creates a task that immediately fails.

Using weights

Weights are used to influence how aggregated progress are calculated for sequential and concurrent executions of tasks. For example

let subtask1 = /* create the task */ .setWeight(1)
let subtask2 = /* create the task */ .setWeight(3)
let subtask3 = /* create the task */ .setWeight(6)

let task = Firefly.sequential([subtask1, subtask2, subtask3])
    .progress { progress in
        print(progress.fraction)
    }
    .execute()

will output 0.1 when subtask1 completes, 0.4 when subtask2 completes and 1.0 when subtask3 completes.

Mapping result, error and outcome of a task

mapResult, mapError and mapOutcome transform – respectively – result, error and outcome of a task.

mapResult is particularly useful when you want to "ignore" the result of a task, i.e. converting Task<SomeDataType, Error> into Task<Void, Error>.

// This method grabs some data, process it
// Once data has been processed, it is no longer needed
// hence returned type is `Task<Void, Error>`
// which is used simply to indicate when the processing has finished / failed
func process() -> Task<Void, Error> {
    let get: Task<Data, Error> = /* get the data */

    return get.then { data in
            // Process the data...
        }
        .mapResult { _ in } // converting into Task<Void, Error>
}   

Using mapProgress

mapProgress transforms a TaskProgress into another TaskProgress.

mapProgress(byScalingFractionIntoRange:) transforms a TaskProgress by "squeezing" its fraction from [0, 1] into another range. For example, in the following

let task = /* create the task */
let mappedTask = task.mapProgress(byScalingFractionIntoRange: (0.6, 1))

when task reports a progress of 0.5 (halfway between 0 and 1), mappedTask will report 0.8 (halfway between 0.6 and 1).

DispatchQueue extensions

DispatchQueue.asyncAfter(deadline:)

A wrapper around DispatchQueue.asyncAfter(deadline:execution:) which returns a task that succeeds after deadline ticks. It is useful for delaying executions. For example

DispatchQueue.main.asyncAfter(deadline: .now() + 0.5)
    .then(task)

delays execution of task by 0.5 seconds.

Using TaskProgress

TaskProgress carries not only the completed fraction of the task, but also any arbitrary extra information, very often a string indicating the phase of the task (e.g. "Downloading", "Extracting", "Processing").

Latest podspec

{
    "name": "Firefly",
    "version": "0.1.1",
    "license": "MIT",
    "summary": "Async that makes sense",
    "homepage": "https://github.com/knguyen2708/Firefly",
    "authors": {
        "Khanh Nguyen": "[email protected]"
    },
    "source": {
        "git": "https://github.com/knguyen2708/Firefly.git",
        "tag": "0.1.1"
    },
    "platforms": {
        "ios": "8.0"
    },
    "source_files": "Firefly/Firefly/Code/*.swift",
    "pushed_with_swift_version": "3.0"
}

Pin It on Pinterest

Share This