Coder Social home page Coder Social logo

izettle / flow Goto Github PK

View Code? Open in Web Editor NEW
231.0 55.0 13.0 563 KB

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

License: MIT License

Ruby 0.17% Swift 99.83%
swift ios flow reactive asynchronous lifecycle functional manual-deployment

flow's Introduction

Platforms Carthage Compatible Swift Package Manager Compatible Xcode version

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

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.

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 [email protected]

flow's People

Contributors

arikis avatar bartszczepaniak avatar carlekman avatar egarni avatar iznunev avatar mansbernhardt avatar mattiasjahnke avatar mattiasjahnke-pp avatar mayur8657 avatar moglistree avatar nataliq-pp avatar niil-ohlin avatar philippotto89 avatar saidsikira avatar shchukin-alex avatar skyewelch avatar ssikira avatar troligtvis avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

flow's Issues

Xcode 10 crashes with ReadWriteSignal

Expected Behavior

The ReadWriteSignal setter should not crash

Current Behavior

ReadWriteSignal crashes when trying to access its setter

Steps to Reproduce

Here is a simple unit test to reproduce the crash

    func testReadWriteSignalCrashing() {
        ReadWriteSignal(CGPoint.zero).value.x = 2 // This one crashes for some reason
    }

Note that these tests are passing:

    func testReadWriteSignalNotCrashing() {
        ReadWriteSignal(CGPoint.zero).value = CGPoint(x: 2, y: 0) // This is fine
        let signal = ReadWriteSignal(CGPoint.zero)
        signal.value.x = 2 // This is also fine
    }

Context

The test fails when building with Xcode 10

Failure Logs

Error:
Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value

[Discussion] Reintroduce write-only signals

I am well aware of the fact that WriteSignal existed in Flow before, but was removed for the following reason:

WriteSignal (previously called Sink) has been used, almost exclusively, as a setter closure, where it has been passed as an argument to Signal.bindTo(). In practice, only its setter has been used and never have we used the fact that a WriteSignal is also a signal.

However, when working on an app built with Flow according to MVVM architecture, I realized that it would be really nice to bring it back. I found it difficult to illustrate this with a code example, so I will try arguing theoretically.


View models are the centerpiece of MVVM. They accept inputs, process them, and produce outputs. Consumers should not care about the way processing is done, so view models can be defined by their inputs and outputs.

MVVM requires that view models have no knowledge of views (to make testing easier, among other things), relying on a binding mechanism that takes care of connecting view outputs and inputs to to the inputs and outputs of a view model instead. Therefore, having an easy to understand and use (i.e. ergonomical) binding mechanism is crucial.

When using Flow for binding, things are fairly straightforward with view model outputs. The outputs of a view model are Signal<T> and ReadSignal<T>, depending on whether an output is stateful or not. As for view model inputs, the following mechanisms can be used (proper disposal of subscriptions is omitted for the sake of brevity):

  • functions (called later directly by view):
struct ViewModel {
    func doSomething(parameter: Int) {
        // processing
    }
}
  • ReadWriteSignal<T>:
struct ViewModel {
    let doSomething: ReadWriteSignal<T>
    init() {
        doSomething.onValue { /* processing */ }
    }
}
  • Callbacker<T>:
struct ViewModel {
    let doSomething: Callbacker<T>
    init() {
        doSomething.addCallback { /* processing */ }
    }
}

These mechanisms cover all possible situations. After all, Flow has been used for years, so I'd be surprised if there were unhandled cases. This made it possible to find a convincing code example. Nonetheless, each of the mechanisms but has its problems:

Functions

  • less convenient binding, must be called in .onValue { } blocks, which is more verbose (.onValue { viewModel.doSomething($0) }) than just using .bindTo(viewModel.doSomething);
  • cannot be (idiomatically) composed with other signals in the view model;
  • defined as methods on the view model, which may require keeping double definitions for the involved output signals (one private writable and one public read-only).

ReadWriteSignal<T>

  • has state, which makes no sense in many cases (for example, input events);
  • difficult to determine whether a ReadWriteSignal<T> defined on a view model is its input or output without inspecting its usage.

Callbacker<T>

This is actually the closest thing Flow has to what I am looking for, but it still is not perfect:

  • less convenient binding, must be called in .onValue { } blocks, which is more verbose (.onValue { viewModel.doSomething.callAll($0) }) than just using bindTo(viewModel.doSomething);
  • cannot be composed with other signals in the view model without taking extra steps (wrapping it in a Signal<T>);
  • it's name makes it stick out, although conceptually this is just a write-only signal.
  • if we were to add a kind of .writeOnly() method to ReadWriteSignal<T> as a way to restrict the read access, it would be strange if it returned a Callbacker<T> instead of a WriteSignal<T>. Using

During the internal discussion, @niil-ohlin suggested making Callbacker<T> a signal provider, which would invalidate the first 2 points, but the remaining 2 are still valid. I would argue that having WriteSignal<T> instead of Callbacker<T> would make the API of Flow cleaner, more symmetrical and easier to understand (I'm saying "instead", since there's probably no point in keeping both).

Other libraries

I looked at RxSwift and the way it handles mutability. There is a concept of Subject, which is

a sort of bridge or proxy [...] that acts both as an observer and as an Observable. Because it is an observer, it can subscribe to one or more Observables, and because it is an Observable, it can pass through the items it observes by reemitting them, and it can also emit new items.

This looks like an equivalent of ReadWriteSignal<T> in Flow.

RxSwift supports giving write-only access to a Subject by transforming it into an AnyObserver<T>. I looked at a few code examples, and there seems to be a pattern where inputs of a view model are defined using a combination of a private Subject<T> and a public AnyObserver<T> (examples: [1], [2], [3], and so on), so the idea of using write-only signals as view model inputs is not meritless.


To summarize, Flow is robust, but in some cases the APIs it provides are not optimal. One of these is the case of defining the input signals for view models. Reintroducing WriteSignal<T> should make this task easier. Admittedly, the gain in each particular case is small, but, due to pervasive use of bindings in MVVM, the benefits will add up.

Comments? Thoughts? Suggestions?

What is the advantage of using Flow over RxSwift?

I am curios to know what benefit it has to offer over RxSwift or Rx* as I can see most of the design patterns it has follow the same structure as Rx.
If someone is using RxSwift why he/she should use this one a comparison benchmark would be good...

[Discussion] Moving Delegates from Form to Flow?

We have several delegates in Form that perhaps should be part of Flow instead as they are basically about asynchronous programming using Signal's and Delegate's:

TextFieldDelegate
ScrollViewDelegate
GestureRecognizerDelegate

We also have delegate and data sources for table view and table kit. But these are currently dependant on TableIndex and the underlying Table data model.

TableViewDelegate
TableViewDataSource
CollectionViewDelegate
CollectionViewDataSource

Perhaps these should be split into two parts, a base part (part of Flow) not depending on any table model and hence will be similar to ScrollViewDelegate and friends. And a sub class or composition for Form where the delegates are refined to make them more convenient to work with the Table data model?

Add a signal method for holding a bag?

I'm wondering whether it is a good idea to add a helper to a signal for holding a dispose bag. I wanted to write down the case to clarify it for myself as well as I'm not sure whether it's a good practice.

I noticed at few places in our codebase we used signal.atValue { _ in _ = bag }. Initially I didn't know what that is, later learnt it will hold the bag until the signal is alive but it still doesn't read in a very straightforward way for me. So question is, is this a bad practice or should we have a helper for it.

Here is the (simplified) case:

struct MyModel {
    let text: String?

    func materialize() -> (UIView, Signal<String>) {
        let textField = UITextField()
        textField.text = text

        let disposable = textField.copySignal.onValue {
            // do something
        }

        // someone needs to hold the disposable

        let view = UIStackView(arrangedSubviews: [textField]) 
        // in the real world more logic goes here but the user of the API usually cares only about the text events
        return (view, textField.providedSignal.plain().atValue { _ in _ = disposable })
    }
}

which we can change to:

return (view, textField.providedSignal.plain().holding(disposable))

and add some tests that make sure that the bag gets disposed.

Warnings: Initialization of 'PThreadMutex' results in a dangling pointer

Expected Behavior

No warnings produced by the framework

Current Behavior

13 compiler warnings

Steps to Reproduce

Always reproducible when building.

Context

Tried to address it here: #104
@digitaliz did further investigation - Daniel please post info here

Failure Logs

Xcode shows:
Initialization of 'PThreadMutex' (aka 'UnsafeMutablePointer<_opaque_pthread_mutex_t>') results in a dangling pointer
which we started seeing recently.

and the following suggestions

1. Implicit argument conversion from 'pthread_mutex_t' (aka '_opaque_pthread_mutex_t') to 'UnsafeMutablePointer<pthread_mutex_t>' (aka 'UnsafeMutablePointer<_opaque_pthread_mutex_t>') produces a pointer valid only for the duration of the call to 'init(_:)'

2. Use 'withUnsafeMutablePointer' in order to explicitly convert argument to pointer valid for a defined scope

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.