Unidirectional State Management Architecture 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
MVVM + FLUX iOS Instagram client in Swift, eliminates Massive View Controller in unidirectional event/state flow manner

CZInstagram MVVM + FLUX iOS Instagram client in Swift, eliminates Massive View Controller in unidirectional event/state flow manner. Unidirectional Da

Cheng Zhang 56 Nov 1, 2022
A super simple library for state management with unidirectional data flow.

OneWay ?? OneWay is still experimental. As such, expect things to break and change in the coming months. OneWay is a super simple library for state ma

SeungYeop Yeom 41 Dec 20, 2022
Unidirectional Data Flow in Swift - Inspired by Redux

ReSwift Supported Swift Versions: Swift 4.2, 5.x For Swift 3.2 or 4.0 Support use Release 5.0.0 or earlier. For Swift 2.2 Support use Release 2.0.0 or

null 7.3k Dec 25, 2022
Unidirectional Data Flow in Swift - Inspired by Redux

ReSwift Supported Swift Versions: Swift 4.2, 5.x For Swift 3.2 or 4.0 Support use Release 5.0.0 or earlier. For Swift 2.2 Support use Release 2.0.0 or

null 7.3k Jan 3, 2023
🟣 Verge is a very tunable state-management engine on iOS App (UIKit / SwiftUI) and built-in ORM.

Verge.swift ?? An effective state management architecture for iOS - UIKit and also SwiftUI ?? _ An easier way to get unidirectional data flow _ _ Supp

VergeGroup 478 Dec 29, 2022
🤖 RxSwift + State Machine, inspired by Redux and Elm.

RxAutomaton RxSwift port of ReactiveAutomaton (State Machine). Terminology Whenever the word "signal" or "(signal) producer" appears (derived from Rea

Yasuhiro Inami 719 Nov 19, 2022
🌾 Harvest: Apple's Combine.framework + State Machine, inspired by Elm.

NOTE: This repository has been discontinued in favor of Actomaton. ?? Harvest Apple's Combine.framework (from iOS 13) + State Machine, inspired by Elm

Yasuhiro Inami 386 Dec 18, 2022
Unidirectional flow implemented using the latest Swift Generics and Swift Concurrency features.

swift-unidirectional-flow Unidirectional flow implemented using the latest Swift Generics and Swift Concurrency features. struct SearchState: Equatabl

Majid Jabrayilov 104 Dec 26, 2022
🔄 Unidirectional data flow in Swift.

Reactor Reactor is a framework for making more reactive applications inspired by Elm, Redux, and recent work on ReSwift. It's small and simple (just o

Reactor 175 Jul 9, 2022
Redux for Swift - a predictable state container for Swift apps

Merge / deprecation announcement: ReduxKit and Swift-Flow have joined forces! The result is ReSwift. The nitty gritty: We decided to deprecate ReduxKi

null 613 Jan 3, 2023
CMPSC475 Final Project, ArboretumID Application allows users to explore the Penn State Arboretum, identify plants and learn about the exhibits!

ArboretumID: CMPSC475 Final Project Taylan Unal (@taylanu) About ArboretumID ArboretumIdentifier (ArboretumID) is an app that enhances the Penn State

Taylan 1 Nov 27, 2021
Predictable state container for Swift too

ReduxSwift ReduxSwift is a minimal Swift port of Redux, a popular JavaScript library for application state management. Functionality Centralized State

Lucas Sunsi Abreu 38 Oct 6, 2020
Elegant state machine for Swift.

SwiftState Elegant state machine for Swift. Example enum MyState: StateType { case state0, state1, state2 } // setup state machine let machine = S

ReactKit 885 Dec 16, 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
An experimental time traveling state store for SwiftUI

SwiftUI Time Travel A SwiftUI state store and view that allow you to scrub through an application's state. This is a super rough prototype: it's only

Tim Donnelly 139 Sep 14, 2022
A powerful, minimal and composable architecture for building reactive iOS apps with SwiftUI or UIKit

SourceArchitecture A simple yet powerful framework for reactive programming with only a minimal optimized set of types. Sources are self-contained, hi

Daniel Hall 6 Nov 1, 2022
Sample iOS application in SwiftUI presenting Redux architecture

SwiftUI-Redux-Demo Sample iOS application in SwiftUI presenting Redux architecture. My full article about Redux in detail you will find here: Redux ar

Wojciech Kulik 25 Nov 27, 2022
💎 Redux like architecture for SwiftUI

Simple Architecture like Redux Installation SPM dependencies: [ .package(url: "https://github.com/gre4ixin/ReduxUI.git", .upToNextMinor(from: "1.0

Pavel 38 Dec 13, 2022
Dynamic and type-safe framework for building linear and non-linear flows.

FlowKit FlowKit is a dynamic flow framework capable of building a flow, based on conditions and ordered according to a logic of next steps. By using F

N26 55 Dec 20, 2022