A Store-Pattern based data-flow architecture. Highly inspired by Vuex
The concept of Verge Store is inspired by Redux, Vuex and ReSwift.
Recenlty, facebookexperimental/Recoil has been opened.
Atoms and Selectors similar to Derived
.
Plus, releasing from so many definition of the actions.
To be more Swift's Style on the writing code.
store.myOperation()
instead of store.dispatch(.myOperation)
The characteristics are
- Method based dispatching action
- Separating the code in a large app
- Emits any events that isolated from State It's for SwiftUI's onReceive(:)
- Logging (Commit, Performance monitoring)
- Binding with Combine and RxSwift
- Normalizing the state with ORM
- Multi-threading
π Differences between other Flux libraries in iOS
Firstly, Verge provides the functions to keep excellent performance in using Store-Pattern.
Verge focuses on using in the real-world.
For example, the application must have many features depends on its business.
Such as the application might be getting complicated.
To solve this issue, we can choose Store-Pattern such as flux.
At a glance, Flux architecture is amazing.
However, we have to follow the disadvantages behind it.
They are coming from the application runs with Data-Driven (Mostly).
Data-Driven will cause some expensive calculations in the application that depends on the complexity of the application.
Sometimes, we may face some performance issues we can't overlook it.
Redux and Vuex are already following that.
- Redux
- reselect
- ORM
- Vuex
- Getters
- ORM
Verge is trying to do that in iOS application with Swift.
Specifically:
- Derived (Similar to facebookexperimental/Recoil's Atom and Selector)
- ORM
π Example code
struct State: StateType {
var name: String = ""
var age: Int = 0
}
enum Activity {
case somethingHappen
}
// πwith UIKit
class ViewController: UIViewController {
...
let store = Store<State, Activity>(initialState: .init(), logger: nil)
...
func update(changes: Changes<State>) {
changes.ifChanged(\.name) { (name) in
nameLabel.text = name
}
changes.ifChanged(\.age) { (age) in
ageLabel.text = age.description
}
}
}
// πwith SwiftUI
struct MyView: View {
@EnvironmentObject var store: Store<State, Activity>
var body: some View {
Group {
Text(store.state.name)
Text(store.state.age)
}
}
}
To start to use Verge in our app, we use these domains:
- State
- A type of state-tree that describes the data our feature needs.
- Activity
- A type that describes an activity that happens during performs the action.
- This instance won't be stored in anywhere. It would help us to perform something by event-driven.
- Consider to use this depends on that if can be represented as a state.
- For example, to present alert or notifcitaions by the action.
- Action
- Just a method that a store or dispatcher defines.
- Store
- A storage object to manage a state and emit activities by the action.
- Store can dispatch actions to itself.
- Dispatcher (Optional)
- A type to dispatch an action to specific store.
- For a large application, to separate the logics each domain.
Setup a Store
Define a state
struct MyState {
var count = 0
}
Define an activity
enum MyActivity {
case countWasIncremented
}
Define a store that uses defined state and activity
class MyStore: Store<MyState, MyActivity> {
init(dependency: Dependency) {
super.init(initialState: .init(), logger: nil)
}
}
We can create an instance from Store
but we can put some dependencies (e.g. API client) with creating a sub-class of Store
.
(If you don't need Activity, you can set Never
there.)
And then, add an action in the store
class MyStore: Store<MyState, MyActivity> {
init(dependency: Dependency) {
super.init(initialState: .init(), logger: nil)
}
func incrementCount() {
commit {
$0.count += 1
}
}
}
Yes, this point is most different with Redux. it's close to Vuex.
Store knows what the application's needs.
For example, call that action.
let store = MyStore(...)
store.incrementCount()
There are some advantages:
- Better Performance
- Swift can perform this action with Swift's method dispatching instead switch-case computing.
- Returns anything we need
- the action can return anything from that action (e.g. state or result)
- If that action dispatch async operation, it can return
Future
object. (such as Vuex action)
Perform a commit asynchronously
func incrementCount() {
DispatchQueue.main.async {
commit {
$0.count += 1
}
}
}
Send an activity from the action
func incrementCount() {
commit {
$0.count += 1
}
send(.countWasIncremented)
}
Use the store in SwiftUI
(Currently, Verge's development is focusing on UIKit.)
struct MyView: View {
@EnvironmentObject var store: MyStore
var body: some View {
Group {
Text(store.state.name)
Text(store.state.age)
}
.onReceive(session.store.activityPublisher) { (activity) in
...
}
}
}
Use the store in UIKit
In UIKit, UIKit doesn't work with differentiating.
To keep better performance, we need to set a value if it's changed.
Verge publishes an object that contains previous state and latest state, Changes object would be so helpful to check if a value changed.
class ViewController: UIViewController {
let store: MyStore
var cancellable: VergeAnyCancellable?
init(store: MyStore) {
...
self.cancellable = store.sinkChanges { [weak self] changes in
self?.update(changes: changes)
}
}
private func update(changes: Changes<MyStore.State>) {
changes.ifChanged(\.name) { (name) in
nameLabel.text = name
}
changes.ifChanged(\.age) { (age) in
ageLabel.text = age.description
}
}
}
Adding a cachable computed property in a State
We can add a computed property in a state to get a derived value with stored property,
and that computed property works fine as well other stored property.
struct MyState {
var items: [Item] = [] {
var itemsCount: Int {
items.count
}
}
However, this patterns might cause an expensive cost of operation depends on how they computes.
To solve it, Verge arrows us to define the computed property with another approach.
struct MyState: ExtendedStateType {
var name: String = ...
var items: [Int] = []
struct Extended: ExtendedType {
let filteredArray = Field.Computed<[Int]> {
$0.items.filter { $0 > 300 }
}
.ifChanged(selector: \.largeArray)
}
}
let store: MyStore
store.changes.computed.filteredArray
This defined computed array calculates only if changed specified value.
That condition to re-calculate is defined with .ifChanged
method in the example code.
And finally, it caches the result by first-time access and it returns cached value until if the source value changed.
Making a slice of the state (Selector)
We can create a slice object that derives a data from the state.
let derived: Derived<Int> = store.derived(.map(\.count))
// take a value
derived.value
// subscribe a value changes
derived.sinkChanges { (changes: Changes<Int>) in
}
Creating a Dispatcher
Store arrows us to define an action in itself, that might cause gain complexity in supporting a large application.
To solve this, Verge offers us to create an object that dispatches an action to the store.
We can separate the code of actions to keep maintainability.
that also help us to manage a different type of dependencies.
For example, the case of those dependencies different between logged-in and logged-out.
class MyDispatcher: MyStore.Dispatcher {
func moreOperation() {
commit {
...
}
}
}
let store: MyStore
let dispatcher = MyDispatcher(target: store)
Additionally, We can create a dispatcher that focuses the specified sub-tree of the state.
You can check the detail of this from our documentation.
It provides core functions of Store-pattern.
- State supports computed property with caching (like Vuex's Getters)
- Derived object to create derived data from state-tree with performant (like redux/reselect)
It provides the function that manages performant many entity objects.
Technically, using Normalization.
In the application that uses many entity objects, we sure highly recommend using such as ORM using Normalization.
About more detail, https://redux.js.org/recipes/structuring-reducers/normalizing-state-shape
It provides several observable that compatible with RxSwift.
π You can see more detail of Verge on Documentation
SwiftUI's concept is similar to the concept of React, Vue, and Elm.
Therefore, the concept of state management will become to be similar as well.
That is Redux or Vuex and more.
Now, almost of iOS Applications are developed on top of UIKit.
And We can't say SwiftUI is ready for top production.
However, it would change.
It's better to use the state management that fits SwiftUI from now. It's not only for that, current UIKit based applications can get more productivity as well.
With Cocoapods,
VergeStore
pod 'Verge/Store'
VergeORM
pod 'Verge/ORM'
VergeRx
pod 'Verge/Rx'
These are separated with subspecs in Podspec.
After installed, these are merged into single module as Verge
.
To use Verge in your code, define import decralation following.
import Verge
Verge is released under the MIT license.