Swiftpack.co - Package - iZettle/Flow

Build Status Platforms Carthage Compatible Swift Package Manager Compatible

Modern applications often contain complex asynchronous flows and life cycles. Flow is a Swift library aiming to simplify building these by solving three main problems:

Flow was carefully designed to be:

  • Easy to use: APIs are carefully designed for readability and ease of use.
  • Pragmatic: Evolved and designed to solve real problems.
  • Composable: Types compose nicely making building complex flows easy.
  • Performant: Flow has been highly tuned for performance.
  • Concurrent: Flow is thread safe and uses a scheduler model that is easy to reason about.
  • Extensible: Flow was designed to be extensible.
  • Strongly typed: Flow makes use of Swift strong typing to better express intention.
  • Correct: Backed by hundreds of unit tests and field tested for years.

Example usage

In Flow the Disposable protocol is used for lifetime management:

extension UIView {
  func showSpinnerOverlay() -> Disposable {
    let spinner = ...
    addSubview(spinner)
    return Disposer {
      spinner.removeFromSuperview()
    }
  }
}

let disposable = view.showSpinnerOverlay()

disposable.dispose() // Remove spinner

Disposable resources can be collected in a common DisposeBag:

let bag = DisposeBag() // Collects resources to be disposed together

bag += showSpinnerOverlay()
bag += showLoadingText()

bag.dispose() // Will dispose all held resources

And the Signal<T> type is used for event handling. Signals are provided by standard UI components:

let bag = DisposeBag()

// UIButton provides a Signal<()>
let loginButton = UIButton(...)

bag += loginButton.onValue {
  // Log in user when tapped
}

// UITextField provides a ReadSignal<String>
let emailField = UITextField(...)
let passwordField = UITextField(...)

// Combine and transform signals
let enableLogin: ReadSignal<Bool> = combineLatest(emailField, passwordField)
  .map { email, password in
    email.isValidEmail && password.isValidPassword
  }

// Use bindings and key-paths to update your UI on changes
bag += enableLogin.bindTo(loginButton, \.isEnabled)

And finally the Future<T> type handles asynchronous operations:

func login(email: String, password: String) -> Future<User> {
  let request = URLRequest(...)
  return URLSession.shared.data(for: request).map { data in
    User(data: data)
  }
}

login(...).onValue { user in
  // Handle successful login
}.onError { error in
  // Handle failed login
}

These three types come with many extensions that allow us to compose complex UI flows:

class LoginController: UIViewController {
  let emailField: UITextField
  let passwordField: UITextField
  let loginButton: UIButton
  let cancelButton: UIBarButtonItem

  var enableLogin: ReadSignal<Bool> { /* Introduced above */ }
  func login(email: String, password: String) -> Future<User> { /* Introduced above */ }
  func showSpinnerOverlay() -> Disposable { /* Introduced above */ }

  // Returns future that completes with true if user chose to retry
  func showRetryAlert(for error: Error) -> Future<Bool> { ... }

  // Will setup UI observers and return a future completing after a successful login
  func runLogin() -> Future<User> {
    return Future { completion in // Complete the future by calling this with your value
      let bag = DisposeBag() // Collect resources to keep alive while executing

      // Make sure to signal at once to set up initial enabled state
      bag += enableLogin.atOnce().bindTo(loginButton, \.isEnabled)  

      // If button is tapped, initiate potentially long running login request using input
      bag += combineLatest(emailField, passwordField)
        .drivenBy(loginButton)
        .onValue { email, password in
          login(email: email, password: password)
            .performWhile {
              // Show spinner during login request
              showSpinnerOverlay()
            }.onErrorRepeat { error in
              // If login fails with an error show an alert...
              // ...and retry the login request if the user chooses to
              showRetryAlert(for: error)
            }.onValue { user in
              // If login is successful, complete runLogin() with the user
              completion(.success(user))
        }
      }

      // If cancel is tapped, complete runLogin() with an error
      bag += cancelButton.onValue {
        completion(.failure(LoginError.dismissed))
      }

      return bag // Return a disposable to dispose once the future completes
    }
  }
}

Requirements

  • Xcode 9.3+
  • Swift 4.1
  • Platforms:
    • iOS 9.0+
    • macOS 10.11+
    • tvOS 9.0+
    • watchOS 2.0+
    • Linux

Installation

Carthage

github "iZettle/Flow" >= 1.0

Cocoa Pods

platform :ios, '9.0'
use_frameworks!

target 'Your App Target' do
  pod 'FlowFramework', '~> 1.0'
end

Swift Package Manager

import PackageDescription

let package = Package(
  name: "Your Package Name",
  dependencies: [
      .Package(url: "https://github.com/iZettle/Flow.git",
               majorVersion: 1)
  ]
)

Introductions

Introductions to the main areas of Flow can be found at:

To learn even more about available functionality you are encouraged to explore the source files that are extensively documented. Code-completion should also help you to discover many of the transformations available on signals and futures.

Learn more

To learn more about the design behind Flow's APIs we recommend reading the following articles. They go more into depth about why Flow's types and APIs look and behave the way they do and give you some insights into how they are implemented:

And to learn how other frameworks can be built using Flow:

Frameworks built on Flow

If your target is iOS, we highly recommend that you also checkout these frameworks that are built on top of Flow:

  • Presentation - Formalizing presentations from model to result
  • Form - Layout, styling, and event handling

Field tested

Flow was developed, evolved and field-tested over the course of several years, and is pervasively used in iZettle's highly acclaimed point of sales app.

Collaborate

You can collaborate with us on our Slack workspace. Ask questions, share ideas or maybe just participate in ongoing discussions. To get an invitation, write to us at ios-oss@izettle.com

Github

link
Stars: 217

Dependencies

Releases

Flow 1.8.4 - 2020-03-30 12:55:04

  • Fix compilation errors in Xcode 11.4
  • Use Swift.Result instead of custom result implementation
  • Update sample to match readme.

Flow 1.8.3 - 2019-10-29 13:18:17

  • Fix race conditions for iOS 13 for Signal and cleanup CallbackState
  • Add new combiner driven(by:) that makes a ReadSignal emit its latest readable values when the given "driver" signal fires events. The combined signal will derive its signal kind from the driver, but without write access.

Flow 1.8.2 - 2019-07-24 14:57:12

  • Fix the traitCollectionWithFallback behaviour on iOS 13 to return the view's predicted traits and prior iOS 13 to respect the key window's traits before falling back to the main screen traits.

Flow 1.8.1 - 2019-07-04 14:49:12

  • Added signal transformations contains(where:) and allSatisfy(where:) as wrappers for boolean reduce() transforms.
  • Changed reduce() implementation to consider initial values when performed on readable signals.

Flow 1.8.0 - 2019-04-29 13:27:49

  • Added deallocSignal(for:) and NSObject.deallocSignal for listen on deallocation of objects.
  • Added signal transformation with(weak:) as a convenience helper for breaking retain cycles.

Flow 1.7.0 - 2019-04-01 09:39:24

  • Migrate to swift 5.

Flow 1.6.0 - 2019-03-21 13:00:18

  • Addition: Make Callbacker conform to SignalProvider.

Flow 1.5.2 - 2019-03-04 11:17:21

  • Bugfix: Make sure shared() updates its last value before calling out to get correct results in case of recursion.

Flow 1.5.1 - 2019-02-13 12:10:14

  • Updates combineLatest to no longer allow mixing of plain and readable signals as for the returned signal to guarantee to be readable all provided signals must as be readable as well. This is technically a breaking of the API, but as the existing implementation is broken and might result in run-time crashes, this change can be considered as a bug-fix.

Flow 1.5.0 - 2018-12-14 15:30:20

  • Added new Future and Signal delay alternatives that accepts a closure that returns the delay based on the value allow variable delays.
  • Fixed a couple of memory leaks.

Flow 1.4.2 - 2018-12-05 15:02:57

  • Fixed a bug with some of Future's repeat methods where the delay delayBetweenRepetitions was added after the last repetition as well.
  • Fixed bug where the predicate passed to onResultRepeat was not always scheduled correctly.

Flow 1.4.1 - 2018-11-09 11:44:43

  • Fixes a problem where onErrorRepeat would not respect the specified delay interval.
  • Fixed issues with tvOS support.
  • Updates some transforms such as toVoid() to schedule on .none instead of .current so these transforms won't cause a re-schedule.

Flow 1.4.0 - 2018-10-16 08:22:54

  • Updated the Signal.flatMapLatest() transformation to allow more flexible mixing of signal types between self and the signal returned from transform.
  • Added Signal.toogle() method for read-write boolean signals.
  • Added didWrite() transformations to read-write signals.
  • Added UIControl valueChanged that will signal with the latest value when the control event .valueChanged is signaled.

Flow 1.3.1 - 2018-10-12 08:21:06

  • Bug-fix: Updated Future.abort(forFutures:) to more correctly handle repetition.

Flow 1.3.0 - 2018-10-02 08:05:17

  • Added versions of bindTo() that can bind a non optional to an optional value.
  • Added enable() to Enablable similar as disable().
  • Added more defaulted parameters to Scheduler.init for dispatch queues.
  • Fixes a crash on Swift 4.2 when immediately (on the same line) modifying a ReadWriteSignal's value.
  • Added Scheduler perform helper.
  • Added signal withLatestFrom transformation.

Flow 1.2.1 - 2018-08-31 13:05:04

  • Conditionally conform Either to Hashable when Left and Right conforms to Hashable.
  • Conforms UISlider to SignalProvider.

Flow 1.2.0 - 2018-06-29 07:48:11

  • Added NSManagedObjectContext.scheduler property for scheduling work on managed object contexts when CoreData is available.

Flow 1.1.0 - 2018-04-17 13:12:13

  • Added DisposeBag.hold() convenience method for holding a reference to an object.
  • Added UITextField delegates for shouldEndEditing and shouldReturnl
  • Added UITextField.isEditingSignal signal.
  • Added UIView.install() for installing gesture recognizers.
  • Added UIView signals for displaying editing menu for copy, cut and paste.
  • Added orientationSignal that will signal on orientation changes.
  • Added UIRefreshControl animate() and refersh() helpers.
  • Added disableActiveEventListeners() helper