Skip to content
/ Flow Public
forked from iZettle/Flow

Flow is a Swift library for working with asynchronous flows and life cycles

License

Notifications You must be signed in to change notification settings

jdmoreira/Flow

 
 

Repository files navigation

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() // Hide spinner

And the Signal<T> type is used for event handling:

let bag = DisposeBag() // Bag of disposables

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

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

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

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

// 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() -> 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 // Completion to call with the result  
      let bag = DisposeBag() // Resources to keep alive while executing 
         
      // Make sure to signal at once to set up initial enabled state
      bag += self.enableLogin.atOnce().bindTo(self.loginButton, \.isEnabled)  

      // If button is tapped, initiate potentially long running login request
      bag += self.loginButton.onValue {
        self.login()
          .performWhile { 
            // Show spinner during login request
            self.showSpinnerOverlay() 
          }.onErrorRepeat { error in
            // If login fails with an error show an alert...
            // ...and retry the login request if the user chose to
            self.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 += self.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

github "iZettle/Flow" >= 1.0
platform :ios, '9.0'
use_frameworks!

target 'Your App Target' do
  pod 'FlowFramework', '~> 1.0'
end
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.

About

Flow is a Swift library for working with asynchronous flows and life cycles

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Swift 99.4%
  • Other 0.6%