{"id":1832,"url":"https://github.com/ra1028/VueFlux","last_synced_at":"2025-08-06T13:32:23.149Z","repository":{"id":56926140,"uuid":"108244644","full_name":"ra1028/VueFlux","owner":"ra1028","description":":recycle: Unidirectional State Management Architecture for Swift - Inspired by Vuex and Flux","archived":false,"fork":false,"pushed_at":"2019-03-28T15:51:13.000Z","size":1013,"stargazers_count":332,"open_issues_count":3,"forks_count":17,"subscribers_count":8,"default_branch":"master","last_synced_at":"2024-05-29T10:20:43.024Z","etag":null,"topics":["architecture","flux","ios","reactive","reactive-programming","redux","swift","unidirectional","unidirectional-data-flow","vuex"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ra1028.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-10-25T08:54:40.000Z","updated_at":"2024-05-14T13:47:34.000Z","dependencies_parsed_at":"2022-08-20T22:40:08.493Z","dependency_job_id":null,"html_url":"https://github.com/ra1028/VueFlux","commit_stats":null,"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ra1028%2FVueFlux","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ra1028%2FVueFlux/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ra1028%2FVueFlux/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ra1028%2FVueFlux/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ra1028","download_url":"https://codeload.github.com/ra1028/VueFlux/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228905472,"owners_count":17989772,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["architecture","flux","ios","reactive","reactive-programming","redux","swift","unidirectional","unidirectional-data-flow","vuex"],"created_at":"2024-01-05T20:15:56.863Z","updated_at":"2024-12-09T14:30:52.239Z","avatar_url":"https://github.com/ra1028.png","language":"Swift","funding_links":[],"categories":["Libs","vue","Reactive Programming","Events [🔝](#readme)"],"sub_categories":["Events","Other free courses","Prototyping","Other Parsing"],"readme":"![VueFlux](./assets/logo.png)\n\n\u003cH4 align=\"center\"\u003e\nUnidirectional State Management Architecture for Swift - Inspired by \u003ca href=\"https://github.com/vuejs/vuex\"\u003eVuex\u003c/a\u003e and \u003ca href=\"https://github.com/facebook/flux\"\u003eFlux\u003c/a\u003e\n\u003c/H4\u003e\n\u003c/br\u003e\n\n\u003cp align=\"center\"\u003e\n\u003ca href=\"https://developer.apple.com/swift\"\u003e\u003cimg alt=\"Swift5\" src=\"https://img.shields.io/badge/language-swift5-orange.svg?style=flat\"/\u003e\u003c/a\u003e\n\u003ca href=\"https://travis-ci.org/ra1028/VueFlux\"\u003e\u003cimg alt=\"Build Status\" src=\"https://travis-ci.org/ra1028/VueFlux.svg?branch=master\"/\u003e\u003c/a\u003e\n\u003ca href=\"https://codebeat.co/projects/github-com-ra1028-vueflux-master\"\u003e\u003cimg alt=\"CodeBeat\" src=\"https://codebeat.co/badges/5f422c2e-40b9-4900-a9e9-9c776757b976\"/\u003e\u003c/a\u003e\n\u003c/br\u003e\n\u003ca href=\"https://cocoapods.org/pods/VueFlux\"\u003e\u003cimg alt=\"CocoaPods\" src=\"https://img.shields.io/cocoapods/v/VueFlux.svg\"/\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/Carthage/Carthage\"\u003e\u003cimg alt=\"Carthage\" src=\"https://img.shields.io/badge/Carthage-compatible-yellow.svg?style=flat\"/\u003e\u003c/a\u003e\n\u003ca href=\"https://developer.apple.com/swift/\"\u003e\u003cimg alt=\"Platform\" src=\"https://img.shields.io/badge/platform-iOS%20%7C%20OSX%20%7C%20tvOS%20%7C%20watchOS-green.svg\"/\u003e\u003c/a\u003e\n\u003ca href=\"https://github.com/ra1028/VueFlux/blob/master/LICENSE\"\u003e\u003cimg alt=\"Lincense\" src=\"http://img.shields.io/badge/license-MIT-000000.svg?style=flat\"/\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n---\n\n## Introduction\nVueFlux is the architecture to manage state with unidirectional data flow for Swift, inspired by [Vuex](https://github.com/vuejs/vuex) and [Flux](https://github.com/facebook/flux).  \n\nIt serves multi store, so that all ViewControllers have designated stores, with rules ensuring that the states can only be mutated in a predictable fashion.  \n\nThe stores also can receives an action dispatched globally.  \nThat makes ViewControllers be freed from dependencies among them.\nAnd, a shared state in an application is also supported by a shared instance of the store.  \n\nAlthough VueFlux makes your projects more productive and codes more readable, it also comes with the cost of more concepts and boilerplates.  \nIf your project is small-scale, you will most likely be fine without VueFlux.  \nHowever, as the scale of your project becomes larger, VueFlux will be the best choice to handle the complicated data flow.  \n\nVueFlux is receives state changes by efficient reactive system. [VueFluxReactive](./VueFluxReactive) is µ reactive framework compatible with this architecture.  \nArbitrary third party reactive frameworks (e.g. [RxSwift](https://github.com/ReactiveX/RxSwift), [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift), etc) can also be used with VueFlux.  \n\n![VueFlux Architecture](./assets/architecture.png)\n\n---\n\n## About VueFlux\nVueFlux makes a unidirectional and predictable flow by explicitly dividing the roles making up the ViewController.\nIt's constituted of following core concepts.  \nState changes are observed by the ViewController using the reactive system.  \nSample code uses VueFluxReactive which will be described later.  \nYou can see example implementation [here](./Example).\n\n- [State](#state)\n- [Actions](#actions)\n- [Mutations](#mutations)\n- [Computed](#computed)\n- [Store](#store)\n\n### State\nThis is the protocol that only just for constraining the type of Action and Mutations, represents the state managed by the Store.  \nImplement some properties of the state, and keeps them readonly by fileprivate access control, like below.   \nWill be mutated only by Mutations, and the properties will be published only by Computed.\n\n```swift\nfinal class CounterState: State {\n    typealias Action = CounterAction\n    typealias Mutations = CounterMutations\n\n    fileprivate let count = Variable(0)\n}\n```\n\n### Actions\nThis is the proxy for functions of dispatching Action.  \nThey can have arbitrary operations asynchronous such as request to backend API.  \nThe type of Action dispatched from Actions' function is determined by State.  \n\n```swift\nenum CounterAction {\n    case increment, decrement\n}\n```\n```swift\nextension Actions where State == CounterState {\n    func increment() {\n        dispatch(action: .increment)\n    }\n\n    func decrement() {\n        dispatch(action: .decrement)\n    }\n}\n\n```\n\n### Mutations\nThis is the protocol that represents `commit` function that mutate the state.  \nBe able to change the fileprivate properties of the state by implementing it in the same file.  \nThe only way to actually change State in a Store is committing an Action via Mutations.  \nChanges of State must be done **synchronously**.\n\n```swift\nstruct CounterMutations: Mutations {\n    func commit(action: CounterAction, state: CounterState) {\n        switch action {\n        case .increment:\n            state.count.value += 1\n\n        case .decrement:\n            state.count.value -= 1\n        }\n    }\n}\n```\n\n### Computed\nThis is the proxy for publishing read-only properties of State.  \nBe able to access and publish the fileprivate properties of state by implementing it in the same file.  \nProperties of State in the Store can only be accessed via this.  \n\n```swift\nextension Computed where State == CounterState {\n    var countTextValues: Signal\u003cString\u003e {\n        return state.count.signal.map { String($0) }\n    }\n}\n```\n\n### Store\nThe Store manages the state, and also can be manage shared state in an application by shared store instance.  \nComputed and Actions can only be accessed via this. Changing the state is the same as well.  \nAn Action dispatched from the `actions` of the instance member is mutates only the designated store's state.  \nOn 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.  \nStore implementation in a ViewController is like as follows:  \n\n```swift\nfinal class CounterViewController: UIViewController {\n    @IBOutlet private weak var counterLabel: UILabel!\n\n    private let store = Store\u003cCounterState\u003e(state: .init(), mutations: .init(), executor: .queue(.global()))\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        store.computed.countTextValues.bind(to: counterLabel, \\.text)\n    }\n\n    @IBAction func incrementButtonTapped(sender: UIButton) {\n        store.actions.increment()  // Store\u003cCounterState\u003e.actions.increment()\n    }\n\n    @IBAction func decrementButtonTapped(sender: UIButton) {\n        store.actions.decrement()  // Store\u003cCounterState\u003e.actions.decrement()\n    }\n}\n```\n\n---\n\n## About VueFluxReactive\nVueFluxReactive is a μ reactive system for observing state changes.  \nIt was made for replacing the existing reactive framework that takes high learning and introduction costs though high-powered such as RxSwift and ReactiveSwift.  \nBut, of course, VueFlux can be used with those framework because VueFluxReactive is separated.  \nVueFluxReactive is constituted of following primitives.  \n\n- [Variable](#variable)\n- [Constant](#constant)\n- [Sink](#sink)\n- [Signal](#signal)\n\n### Sink\nThis type has a way of generating Signal.  \nOne can send values into a sink and receives it by observing generated signal.  \nSignals generated from Sink does not hold the latest value.  \nPractically, it's used to send commands (such as presents another ViewController) from State to ViewController.  \n**Can't deliver values recursively**.  \n\n```swift\nlet sink = Sink\u003cInt\u003e()\nlet signal = sink.signal\n\nsignal.observe { print($0) }\n\nsink.send(value: 100)\n\n// prints \"100\"\n```\n\n### Signal\nA push-driven stream that sends value changes over time.  \nValues will be sent to all registered observers at the same time.  \nAll of values changes are made via this primitive.  \n\n```swift\nlet sink = Sink\u003cInt\u003e()\nlet signal = sink.signal\n\nsignal.observe { print(\"1: \\($0)\") }\nsignal.observe { print($2: \\($0)\") }\n\nsink.send(value: 100)\nsink.send(value: 200)\n\n// prints \"1: 100\"\n// prints \"2: 100\"\n// prints \"1: 200\"\n// prints \"2: 200\"\n```\n\n### Variable\nVariable represents a thread-safe mutable value that allows observation of its changes via signal generated from it.  \nThe signal forwards the latest value when observing starts. All value changes are delivers on after that.  \n**Can't deliver values recursively**.  \n\n```swift\nlet variable = Variable(0)\n\nvariable.signal.observe { print($0) }\n\nvariable.value = 1\n\nprint(variable.value)\n\nvariable.signal.observe { print($0) }\n\n/// prints \"0\"\n/// prints \"1\"\n/// prints \"1\"\n/// prints \"1\"\n```\n\n### Constant\nThis is a kind of wrapper to making Variable read-only.  \nConstant generated from Variable reflects the changes of its Variable.  \nJust like Variable, the latest value and value changes are forwarded via signal. But Constant is not allowed to be changed directly.  \n\n```swift\nlet variable = Variable(0)\nlet constant = variable.constant\n\nconstant.signal.observe { print($0) }\n\nvariable.value = 1\n\nprint(constant.value)\n\nconstant.signal.observe { print($0) }\n\n/// prints \"0\"\n/// prints \"1\"\n/// prints \"1\"\n/// prints \"1\"\n```\n\n---\n\n## Advanced Usage\n\n### Executor\nExecutor determines the execution context of function such as execute on main thread, on a global queue and so on.  \nSome contexts are built in default.  \n\n- immediate  \n  Executes function immediately and synchronously.  \n\n- mainThread  \n  Executes immediately and synchronously if execution thread is main thread. Otherwise enqueue to main-queue.\n\n- queue(_ dispatchQueue: DispatchQueue)  \n  All functions are enqueued to given dispatch queue.  \n\nIn the following case, the store commits actions to mutations on global queue.  \n\n```swift\nlet store = Store\u003cCounterState\u003e(state: .init(), mutations: .init(), executor: .queue(.global()))\n```\n\nIf you observe like below, the observer function is executed on global background queue.  \n\n```swift\nstore.computed.valueSignal\n    .observe(on: .queue(.global(qos: .background)))\n    .observe { value in\n        // Executed on global background queue\n}\n```\n\n### Signal Operators\nVueFluxReactive restricts functional approach AMAP.  \nHowever, includes minimum operators for convenience.  \nThese operators transform a signal into a new sinal generated in the operators, which means the invariance of Signal holds.  \n\n**map**  \nThe map operator is used to transform the values in a signal.  \n\n```swift\nlet sink = Sink\u003cInt\u003e()\nlet signal = sink.signal\n\nsignal\n    .map { \"Value is \\($0)\" }\n    .observe { print($0) }\n\nsink.send(value: 100)\nsink.send(value: 200)\n\n// prints \"Value is 100\"\n// prints \"Value is 200\"\n```\n\n**observe(on:)**  \nForwards all values ​​on context of a given Executor.  \n\n```swift\nlet sink = Sink\u003cInt\u003e()\nlet signal = sink.signal\n\nsignal\n    .observe(on: .mainThread)\n    .observe { print(\"Value: \\($0), isMainThread: \\(Thread.isMainThread)\") }\n\nDispatchQueue.global().async {\n    sink.send(value: 100)    \n    sink.send(value: 200)\n}\n\n// prints \"Value: 100, isMainThread: true\"\n// prints \"Value: 200, isMainThread: true\"\n```\n\n### Disposable\nDisposable represents something that can be disposed, usually unregister a observe that registered to Signal.  \n\n```swift\nlet disposable = signal.observe { value in\n    // Not executed after disposed.\n}\n\ndisposable.dispose()\n```\n\n### DisposableScope\nDisposableScope serves as resource manager of Disposable.  \nThis will terminate all added disposables on deinitialization or disposed.  \nFor example, when the ViewController which has a property of DisposableScope is dismissed, all disposables are terminated.  \n\n```swift\nvar disposableScope: DisposableScope? = DisposableScope()\n\ndisposableScope += signal.observe { value in\n    // Not executed after disposableScope had deinitialized.\n}\n\ndisposableScope = nil  // Be disposed\n```\n\n### Scoped Observing\nIn observing, you can pass `AnyObject` as the parameter of `duringScopeOf:`.  \nAn observer function which is observing the Signal will be dispose when the object is deinitialize.  \n\n```swift\nsignal.observe(duringScopeOf: self) { value in\n    // Not executed after `self` had deinitialized.\n}\n```\n\n### Bind\nBinding makes target object's value be updated to the latest value received via Signal.  \nThe binding is no longer valid after the target object is deinitialized.  \nBindings work on **main thread** by default.  \n\nClosure binding.\n```swift\ntext.signal.bind(to: label) { label, text in\n    label.text = text\n}\n```\n\nSmart KeyPath binding.\n```swift\ntext.signal.bind(to: label, \\.text)\n```\n\nBinder\n```swift\nextension UIView {\n    func setHiddenBinder(duration: TimeInterval) -\u003e Binder\u003cBool\u003e {\n        return Binder(target: self) { view, isHidden in\n            UIView.transition(\n              with: view,\n              duration: duration,\n              options: .transitionCrossDissolve,\n              animations: { view.isHidden = isHidden }\n            )\n        }\n    }\n}\n\nisViewHidden.signal.bind(to: view.setHiddenBinder(duration: 0.3))\n```\n\n### Shared Store\nYou should make a shared instance of Store in order to manages a state shared in application.  \nAlthough you may define it as a global variable, an elegant way is overriding the Store and defining a static member `shared`.  \n\n```swift\nfinal class CounterStore: Store\u003cCounterState\u003e {\n    static let shared = CounterStore()\n\n    private init() {\n        super.init(state: .init(), mutations: .init(), executor: .queue(.global()))\n    }\n}\n```\n\n### Global Dispatch\nVueFlux can also serve as a global event bus.  \nIf 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.  \n\n```swift\nlet store = Store\u003cCounterState\u003e(state: .init(), mutations: .init(), executor: .immediate)\n\nprint(store.computed.count.value)\n\nStore\u003cCounterState\u003e.actions.increment()\n\nprint(store.computed.count.value)\n\n// prints \"0\"\n// prints \"1\"\n```\n---\n\n## Requirements\n- Swift4.1+\n- OS X 10.9+\n- iOS 9.0+\n- watchOS 2.0+\n- tvOS 9.0+\n\n---\n\n## Installation\n\n### [CocoaPods](https://cocoapods.org/)  \nIf use VueFlux with VueFluxReactive, add the following to your Podfile:  \n```ruby\nuse_frameworks!\n\ntarget 'TargetName' do\n  pod 'VueFluxReactive'\nend\n```\nOr if, use with third-party Reactive framework:  \n```ruby\nuse_frameworks!\n\ntarget 'TargetName' do\n  pod 'VueFlux'\n  # And reactive framework you like\nend\n```\nAnd run\n```sh\npod install\n```\n\n### [Carthage](https://github.com/Carthage/Carthage)  \nAdd the following to your Cartfile:  \n```ruby\ngithub \"ra1028/VueFlux\"\n```\nAnd run\n```sh\ncarthage update\n```\n\n---\n\n## Example Projects\n- [VueFluxExample-GitHub](https://github.com/ra1028/VueFluxExample-GitHub): Simple example using GitHub user search API.  \n\n---\n\n## Contribution\nWelcome to fork and submit pull requests.  \n\nBefore submitting pull request, please ensure you have passed the included tests.  \nIf your pull request including new function, please write test cases for it.  \n\n---\n\n## License\nVueFlux and VueFluxReactive is released under the MIT License.  \n\n---\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fra1028%2FVueFlux","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fra1028%2FVueFlux","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fra1028%2FVueFlux/lists"}