VueFlux is the architecture to manage state with unidirectional data flow for Swift, inspired by Vuex and Flux.

Overview

VueFlux

Unidirectional State Management Architecture for Swift - Inspired by Vuex and Flux


Swift5 Build Status CodeBeat
CocoaPods Carthage Platform Lincense


Introduction

VueFlux is the architecture to manage state with unidirectional data flow for Swift, inspired by Vuex and Flux.

It serves multi store, so that all ViewControllers have designated stores, with rules ensuring that the states can only be mutated in a predictable fashion.

The stores also can receives an action dispatched globally.
That makes ViewControllers be freed from dependencies among them. And, a shared state in an application is also supported by a shared instance of the store.

Although VueFlux makes your projects more productive and codes more readable, it also comes with the cost of more concepts and boilerplates.
If your project is small-scale, you will most likely be fine without VueFlux.
However, as the scale of your project becomes larger, VueFlux will be the best choice to handle the complicated data flow.

VueFlux is receives state changes by efficient reactive system. VueFluxReactive is µ reactive framework compatible with this architecture.
Arbitrary third party reactive frameworks (e.g. RxSwift, ReactiveSwift, etc) can also be used with VueFlux.

VueFlux Architecture


About VueFlux

VueFlux makes a unidirectional and predictable flow by explicitly dividing the roles making up the ViewController. It's constituted of following core concepts.
State changes are observed by the ViewController using the reactive system.
Sample code uses VueFluxReactive which will be described later.
You can see example implementation here.

State

This is the protocol that only just for constraining the type of Action and Mutations, represents the state managed by the Store.
Implement some properties of the state, and keeps them readonly by fileprivate access control, like below.
Will be mutated only by Mutations, and the properties will be published only by Computed.

final class CounterState: State {
    typealias Action = CounterAction
    typealias Mutations = CounterMutations

    fileprivate let count = Variable(0)
}

Actions

This is the proxy for functions of dispatching Action.
They can have arbitrary operations asynchronous such as request to backend API.
The type of Action dispatched from Actions' function is determined by State.

enum CounterAction {
    case increment, decrement
}
extension Actions where State == CounterState {
    func increment() {
        dispatch(action: .increment)
    }

    func decrement() {
        dispatch(action: .decrement)
    }
}

Mutations

This is the protocol that represents commit function that mutate the state.
Be able to change the fileprivate properties of the state by implementing it in the same file.
The only way to actually change State in a Store is committing an Action via Mutations.
Changes of State must be done synchronously.

struct CounterMutations: Mutations {
    func commit(action: CounterAction, state: CounterState) {
        switch action {
        case .increment:
            state.count.value += 1

        case .decrement:
            state.count.value -= 1
        }
    }
}

Computed

This is the proxy for publishing read-only properties of State.
Be able to access and publish the fileprivate properties of state by implementing it in the same file.
Properties of State in the Store can only be accessed via this.

extension Computed where State == CounterState {
    var countTextValues: Signal<String> {
        return state.count.signal.map { String($0) }
    }
}

Store

The Store manages the state, and also can be manage shared state in an application by shared store instance.
Computed and Actions can only be accessed via this. Changing the state is the same as well.
An Action dispatched from the actions of the instance member is mutates only the designated store's state.
On the other hand, an Action dispatched from the actions of the static member will mutates all the states managed in the stores which have same generic type of State in common.
Store implementation in a ViewController is like as follows:

final class CounterViewController: UIViewController {
    @IBOutlet private weak var counterLabel: UILabel!

    private let store = Store<CounterState>(state: .init(), mutations: .init(), executor: .queue(.global()))

    override func viewDidLoad() {
        super.viewDidLoad()

        store.computed.countTextValues.bind(to: counterLabel, \.text)
    }

    @IBAction func incrementButtonTapped(sender: UIButton) {
        store.actions.increment()  // Store.actions.increment()
    }

    @IBAction func decrementButtonTapped(sender: UIButton) {
        store.actions.decrement()  // Store.actions.decrement()
    }
}

About VueFluxReactive

VueFluxReactive is a μ reactive system for observing state changes.
It was made for replacing the existing reactive framework that takes high learning and introduction costs though high-powered such as RxSwift and ReactiveSwift.
But, of course, VueFlux can be used with those framework because VueFluxReactive is separated.
VueFluxReactive is constituted of following primitives.

Sink

This type has a way of generating Signal.
One can send values into a sink and receives it by observing generated signal.
Signals generated from Sink does not hold the latest value.
Practically, it's used to send commands (such as presents another ViewController) from State to ViewController.
Can't deliver values recursively.

let sink = Sink<Int>()
let signal = sink.signal

signal.observe { print($0) }

sink.send(value: 100)

// prints "100"

Signal

A push-driven stream that sends value changes over time.
Values will be sent to all registered observers at the same time.
All of values changes are made via this primitive.

let sink = Sink<Int>()
let signal = sink.signal

signal.observe { print("1: \($0)") }
signal.observe { print($2: \($0)") }

sink.send(value: 100)
sink.send(value: 200)

// prints "1: 100"
// prints "2: 100"
// prints "1: 200"
// prints "2: 200"

Variable

Variable represents a thread-safe mutable value that allows observation of its changes via signal generated from it.
The signal forwards the latest value when observing starts. All value changes are delivers on after that.
Can't deliver values recursively.

let variable = Variable(0)

variable.signal.observe { print($0) }

variable.value = 1

print(variable.value)

variable.signal.observe { print($0) }

/// prints "0"
/// prints "1"
/// prints "1"
/// prints "1"

Constant

This is a kind of wrapper to making Variable read-only.
Constant generated from Variable reflects the changes of its Variable.
Just like Variable, the latest value and value changes are forwarded via signal. But Constant is not allowed to be changed directly.

let variable = Variable(0)
let constant = variable.constant

constant.signal.observe { print($0) }

variable.value = 1

print(constant.value)

constant.signal.observe { print($0) }

/// prints "0"
/// prints "1"
/// prints "1"
/// prints "1"

Advanced Usage

Executor

Executor determines the execution context of function such as execute on main thread, on a global queue and so on.
Some contexts are built in default.

  • immediate
    Executes function immediately and synchronously.

  • mainThread
    Executes immediately and synchronously if execution thread is main thread. Otherwise enqueue to main-queue.

  • queue(_ dispatchQueue: DispatchQueue)
    All functions are enqueued to given dispatch queue.

In the following case, the store commits actions to mutations on global queue.

let store = Store<CounterState>(state: .init(), mutations: .init(), executor: .queue(.global()))

If you observe like below, the observer function is executed on global background queue.

store.computed.valueSignal
    .observe(on: .queue(.global(qos: .background)))
    .observe { value in
        // Executed on global background queue
}

Signal Operators

VueFluxReactive restricts functional approach AMAP.
However, includes minimum operators for convenience.
These operators transform a signal into a new sinal generated in the operators, which means the invariance of Signal holds.

map
The map operator is used to transform the values in a signal.

let sink = Sink<Int>()
let signal = sink.signal

signal
    .map { "Value is \($0)" }
    .observe { print($0) }

sink.send(value: 100)
sink.send(value: 200)

// prints "Value is 100"
// prints "Value is 200"

observe(on:)
Forwards all values ​​on context of a given Executor.

let sink = Sink<Int>()
let signal = sink.signal

signal
    .observe(on: .mainThread)
    .observe { print("Value: \($0), isMainThread: \(Thread.isMainThread)") }

DispatchQueue.global().async {
    sink.send(value: 100)    
    sink.send(value: 200)
}

// prints "Value: 100, isMainThread: true"
// prints "Value: 200, isMainThread: true"

Disposable

Disposable represents something that can be disposed, usually unregister a observe that registered to Signal.

let disposable = signal.observe { value in
    // Not executed after disposed.
}

disposable.dispose()

DisposableScope

DisposableScope serves as resource manager of Disposable.
This will terminate all added disposables on deinitialization or disposed.
For example, when the ViewController which has a property of DisposableScope is dismissed, all disposables are terminated.

var disposableScope: DisposableScope? = DisposableScope()

disposableScope += signal.observe { value in
    // Not executed after disposableScope had deinitialized.
}

disposableScope = nil  // Be disposed

Scoped Observing

In observing, you can pass AnyObject as the parameter of duringScopeOf:.
An observer function which is observing the Signal will be dispose when the object is deinitialize.

signal.observe(duringScopeOf: self) { value in
    // Not executed after `self` had deinitialized.
}

Bind

Binding makes target object's value be updated to the latest value received via Signal.
The binding is no longer valid after the target object is deinitialized.
Bindings work on main thread by default.

Closure binding.

text.signal.bind(to: label) { label, text in
    label.text = text
}

Smart KeyPath binding.

text.signal.bind(to: label, \.text)

Binder

extension UIView {
    func setHiddenBinder(duration: TimeInterval) -> Binder<Bool> {
        return Binder(target: self) { view, isHidden in
            UIView.transition(
              with: view,
              duration: duration,
              options: .transitionCrossDissolve,
              animations: { view.isHidden = isHidden }
            )
        }
    }
}

isViewHidden.signal.bind(to: view.setHiddenBinder(duration: 0.3))

Shared Store

You should make a shared instance of Store in order to manages a state shared in application.
Although you may define it as a global variable, an elegant way is overriding the Store and defining a static member shared.

final class CounterStore: Store {
    static let shared = CounterStore()

    private init() {
        super.init(state: .init(), mutations: .init(), executor: .queue(.global()))
    }
}

Global Dispatch

VueFlux can also serve as a global event bus.
If you call a function from actions that is a static member of Store, all the states managed in the stores which have same generic type of State in common are affected.

let store = Store<CounterState>(state: .init(), mutations: .init(), executor: .immediate)

print(store.computed.count.value)

Store<CounterState>.actions.increment()

print(store.computed.count.value)

// prints "0"
// prints "1"

Requirements

  • Swift4.1+
  • OS X 10.9+
  • iOS 9.0+
  • watchOS 2.0+
  • tvOS 9.0+

Installation

CocoaPods

If use VueFlux with VueFluxReactive, add the following to your Podfile:

use_frameworks!

target 'TargetName' do
  pod 'VueFluxReactive'
end

Or if, use with third-party Reactive framework:

use_frameworks!

target 'TargetName' do
  pod 'VueFlux'
  # And reactive framework you like
end

And run

pod install

Carthage

Add the following to your Cartfile:

github "ra1028/VueFlux"

And run

carthage update

Example Projects


Contribution

Welcome to fork and submit pull requests.

Before submitting pull request, please ensure you have passed the included tests.
If your pull request including new function, please write test cases for it.


License

VueFlux and VueFluxReactive is released under the MIT License.


Comments
  • Swift4.1 Support

    Swift4.1 Support

    I use VueFlux with Carthage. VueFlux has the error below after update Xcode to 9.3.

    Module compiled with Swift 4.0.3 cannot be imported in Swift 4.1: /Path/to/VueFlux.framework/Modules/VueFlux.swiftmodule/x86_64.swiftmodule
    

    Do you plan to update for this? Thanks

    bug 
    opened by beryu 2
  • Fix Segmentation fault error in Swift4.1

    Fix Segmentation fault error in Swift4.1

    Fixed #24

    It seems that an error has occurred due to a compiler bug. So I avoided this error by changing only the description method. Build succeeded in the following environment.

    [Environment] Xcode9.3, Swift4.1

    opened by naoto0n2 1
  • 1.4.0

    1.4.0

    VueFlux

    Breaking changes

    • Executor.WorkItem now renamed to CancelableProcedure
    • Remove Executor.execute<Value>(workItem:with)

    VueFluxReactive

    Fix

    • Signal now no longer send values to observer after disposed

    Add

    • Add NOP AnyDisposable initializer
    opened by ra1028 0
  • 1.2.0

    1.2.0

    VueFlux

    Enhancement

    Performance of commitng an Action improved 10x or more. Performance of getting a shared instance of Dispatcher has improved 3x or more.

    VueFluxReactive

    Enhancement

    Performance of multicasting values improved about 30x.

    opened by ra1028 0
  • Dispatcher performance

    Dispatcher performance

    Performance of retrieving Dispatcher shared instance improved 3x or more.

    • Class based Dispatcher
    • Use ObjectIdentifier instead of DispatcherContext.Identifier
    opened by ra1028 0
  • 1.1.0

    1.1.0

    VueFlux

    Fix

    • Immediately cancel commiting on no matter what executor when Store deinitialized.

    Breaking Changes

    • Rename ThreadSafe to AtomicReference

    VueFluxReactive

    Enhancement

    • Bindings now work on main thread by default
    opened by ra1028 0
  • Remove unnecessary `State` constraint for reusability

    Remove unnecessary `State` constraint for reusability

    This is just an example code to improve framework's type signatures. (Note: Test fixes are not included)

    • State should not be constrained by any protocols (especially with associated type). Otherwise, it won't be reusable and composable when making a larger state.
    • Same can be said for Action which should be isolated from State.
    • Only Mutation should be the protocol that associates both State and Action
    • extension Actions where Action == CounterAction is more intuitive to make an extension compared to extension Actions where State == CounterState, where CounterAction is implicitly associated inside CounterState.
    opened by inamiy 8
  • Accessing state within actions

    Accessing state within actions

    Vuex allows to access state within actions. This makes it easier to check the current state and make some decisions, do some calculations and then dispatch to modify the state. Without these, I've found actions, in practice, just to be proxy methods forwarding everything to mutators. Is there a way to access the state in actions? If not, it would be very helpful to allow it to access the state.

    question 
    opened by ashokgelal 1
Releases(1.6.0)
  • 1.6.0(Mar 28, 2019)

  • 1.4.1(Apr 17, 2018)

    VueFlux

    • Fix Segmentation fault error in Swift4.1 (https://github.com/ra1028/VueFlux/pull/25, https://github.com/ra1028/VueFlux/issues/24)
    Source code(tar.gz)
    Source code(zip)
  • 1.4.0(Apr 17, 2018)

    VueFlux

    Breaking changes

    • Executor.WorkItem now renamed to CancelableProcedure
    • Remove Executor.execute<Value>(workItem:with)

    VueFluxReactive

    Fix

    • Signal now no longer send values to observer after disposed

    Add

    • Add NOP AnyDisposable initializer
    Source code(tar.gz)
    Source code(zip)
  • 1.3.1(Mar 5, 2018)

  • 1.3.0(Feb 28, 2018)

    VueFlux

    Enhancement

    Significant performance improvement. AtomicReference now able to create instance that possible to recursive locking.

    VueFluxReactive

    Enhancement

    Significant performance improvement.

    Fix

    Prevent deadlock occurred when dispose or start new observing in Signal's observing handler.

    Source code(tar.gz)
    Source code(zip)
  • 1.2.0(Feb 17, 2018)

    VueFlux

    Enhancement

    Performance of commitng an Action improved 10x or more. Performance of getting a shared instance of Dispatcher has improved 3x or more.

    VueFluxReactive

    Enhancement

    Performance of multicasting values improved about 30x.

    Source code(tar.gz)
    Source code(zip)
  • 1.1.0(Feb 7, 2018)

    https://github.com/ra1028/VueFlux/pull/6

    VueFlux

    Fix

    • Immediately cancel commiting on no matter what executor when Store deinitialized.

    Breaking Changes

    • Rename ThreadSafe to AtomicReference

    VueFluxReactive

    Enhancement

    • Bindings now work on main thread by default
    Source code(tar.gz)
    Source code(zip)
  • 1.0.0(Jan 25, 2018)

Owner
Ryo Aoyama
░░░░░░░░░░░░░░░░░░░░░ ░░░░░░░▀▄░░░▄▀░░░░░░░ ░░░░░▄█▀███▀█▄░░░░░ ░░░█▀███████▀█░░░ ░░░█░█▀▀▀▀▀▀▀█░█░░░ ░░░░░░░▀▀░░░▀▀░░░░░░░ ░░░░░░░░░░░░░░░░░░░░░
Ryo Aoyama
A simple and predictable state management library inspired by Flux + Elm + Redux.

A simple and predictable state management library inspired by Flux + Elm + Redux. Flywheel is built on top of Corotuines using the concepts of structured concurrency. At the core, lies the State Machine which is based on actor model.

Abhi Muktheeswarar 35 Dec 29, 2022
A library for reactive and unidirectional Swift applications

ReactorKit is a framework for a reactive and unidirectional Swift application architecture. This repository introduces the basic concept of ReactorKit

ReactorKit 2.5k Dec 28, 2022
A New, Modern Reactive State Management Library for Swift and SwiftUI (The iOS implementation of Recoil)

RecoilSwift RecoilSwift is a lightweight & reactive swift state management library. RecoilSwift is a SwiftUI implementation of recoil.js which powered

Holly Li 160 Dec 25, 2022
RxReduce is a lightweight framework that ease the implementation of a state container pattern in a Reactive Programming compliant way.

About Architecture concerns RxReduce Installation The key principles How to use RxReduce Tools and dependencies Travis CI Frameworks Platform Licence

RxSwift Community 125 Jan 29, 2022
Predictable state management for SwiftUI applications.

SwiftDux Predictable state management for SwiftUI applications. SwiftDux is a state container inspired by Redux and built on top of Combine and SwiftU

Steven Lambion 148 Jul 4, 2022
Tempura - A holistic approach to iOS development, inspired by Redux and MVVM

Tempura is a holistic approach to iOS development, it borrows concepts from Redux (through Katana) and MVVM. ?? Installation Requirements CocoaPods ??

Bending Spoons 692 Dec 17, 2022
Swift Apps in a Swoosh! A modern framework for creating iOS apps, inspired by Redux.

Katana is a modern Swift framework for writing iOS applications' business logic that are testable and easy to reason about. Katana is strongly inspire

Bending Spoons 2.2k Jan 1, 2023
A simple example of the VIPER architecture for iOS apps

Counter Counter is a simple app showing the basics of the VIPER architecture, a version of Uncle Bob’s Clean Architecture for iOS apps. Counter shows

Mutual Mobile 353 Nov 6, 2022
Modern thread-safe and type-safe key-value observing for Swift and Objective-C

Now Archived and Forked PMKVObserver will not be maintained in this repository going forward. Please use, create issues on, and make PRs to the fork o

Postmates Inc. 708 Jun 29, 2022
Define and chain Closures with Inputs and Outputs

Closure Define and chain Closures with Inputs and Outputs Examples No Scoped State let noStateCount = Closure<String, String> { text in String(repea

Zach Eriksen 3 May 18, 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
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
A micro-library for creating and observing events.

Signals Signals is a library for creating and observing events. It replaces delegates, actions and NSNotificationCenter with something much more power

Tuomas Artman 454 Dec 21, 2022
SwiftUI-compatible framework for building browser apps with WebAssembly and native apps for other platforms

SwiftUI-compatible framework for building browser apps with WebAssembly At the moment Tokamak implements a very basic subset of SwiftUI. Its DOM rende

TokamakUI 2k Dec 30, 2022
SwiftUI-compatible framework for building browser apps with WebAssembly and native apps for other platforms

SwiftUI-compatible framework for building browser apps with WebAssembly At the moment Tokamak implements a very basic subset of SwiftUI. Its DOM rende

TokamakUI 2k Dec 29, 2022
Very simple Observable and Publisher implementation for iOS apps.

Very simple Observable and Publisher implementation for iOS apps.

Igor Kulman 7 Jun 11, 2022
Interactive notification pop-over (aka "Toast) modeled after the iOS AirPods and Apple Pencil indicator.

Interactive notification pop-over (aka "Toast) modeled after the iOS AirPods and Apple Pencil indicator. Installation The recommended way is to use Co

Philip Kluz 108 Nov 11, 2022
Bond is a Swift binding framework that takes binding concepts to a whole new level.

Bond, Swift Bond Update: Bond 7 has been released! Check out the migration guide to learn more about the update. Bond is a Swift binding framework tha

Declarative Hub 4.2k Jan 5, 2023
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