Thanks to visit codestin.com
Credit goes to github.com

Skip to content

regexident/Store

Β 
Β 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

𝘚𝘡𝘰𝘳𝘦 Swift Build Status

Dispatch

Unidirectional, transactional, operation-based Store implementation for Swift and SwiftUI

π™ΎπšŸπšŽπš›πšŸπš’πšŽπš 

Store eschews MVC in favour of a unidirectional data flow. When a user interacts with a view, the view propagates an action through a central dispatcher, to the various stores that hold the application's data and business logic, which updates all of the views that are affected.

This works especially well with SwiftUI's declarative programming style, which allows the store to send updates without specifying how to transition views between states.

  • Stores: Holds the state of your application. You can have multiple stores for multiple domains of your app.
  • Actions: You can only perform state changes through actions. Actions are small pieces of data (typically enums or structs) that describe a state change. By drastically limiting the way state can be mutated, your app becomes easier to understand and it gets easier to work with many collaborators.
  • Transaction: A single execution of an action.
  • Views: A simple function of your state. This works especially well with SwiftUI's declarative programming style.

πš‚πšπš˜πš›πšŽ

Stores contain the application state and logic. Their role is somewhat similar to a model in a traditional MVC, but they manage the state of many objects β€” they do not represent a single record of data like ORM models do. More than simply managing a collection of ORM-style objects, stores manage the application state for a particular domain within the application.

This allows an action to result in an update to the state of the store. After the stores are updated, they notify the observers that their state has changed, so the views may query the new state and update themselves.

struct Counter: ModelType {
  var count = 0
}

let store = Store<Counter>()

π™°πšŒπšπš’πš˜πš—

An action represent an operation on the store.

It can be represented using an enum:

enum CounterAction: ActionType {
  case increase
  case decrease

  var identifier: String {
    switch self {
    case .increase: return "INCREASE"
    case .decrease: return "DECREASE"
    }
  }

  func reduce(context: TransactionContext<Store<Counter>, Self>) {
    defer { 
      // Remember to always call `fulfill` to signal the completion of this operation.
      context.fulfill()
    }
    switch self {
    case .increase: context.reduceModel { $0.count += 1 }
    case .decrease: context.reduceModel { $0.count -= 1 }

    }
  }
}

Or a struct:

struct IncreaseAction: ActionType {
  let count: Int
  
  func reduce(context: TransactionContext<Store<Counter>, Self>) {
    defer { 
      // Remember to always call `fulfill` to signal the completion of this operation.
      context.fulfill()
    }
    context.reduceModel { $0.count += 1 }
  }
}

πšƒπš›πšŠπš—πšœπšŠπšŒπšπš’πš˜πš—

A transaction represent an execution of a given action. The dispatcher can run transaction in three different modes: async, sync, and mainThread. Additionally the trailing closure of the run method can be used to run a completion closure for the actions that have had run.

π™ΆπšŽπšπšπš’πš—πš πšœπšπšŠπš›πšπšŽπš

TL;DR

import SwiftUI
import Store

struct Counter: ModelType {
  var count = 0
}

enum CounterAction: ActionType {
  case increase(amount: Int)
  case decrease(amount: Int)

  var identifier: String {
    switch self {
    case .increase(_): return "INCREASE"
    case .decrease(_): return "DECREASE"
    }
  }

  func perform(context: TransactionContext<Store<Counter>, Self>) {
    defer {
      context.fulfill()
    }
    switch self {
    case .increase(let amount):
      context.reduceModel { $0.count += amount }
    case .decrease(let amount):
      context.reduceModel { $0.count -= amount }
    }
  }
}

// MARK: - UI

struct ContentView : View {
  @EnvironmentObject var store: Store<Counter>
  var body: some View {
    Text("counter \(store.model.count)").tapAction {
      store.run(action: CounterAction.increase(amount: 1))
    }
  }
}

// MARK: - Preview

#if DEBUG
struct ContentView_Previews : PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(Store<Counter>())
    }
}
#endif

π™Όπš’πšπšπš•πšŽπš πšŠπš›πšŽ

Middleware objects must conform to:

public protocol MiddlewareType: class {
  /// A transaction has changed its state.
  func onTransactionStateChange(_ transaction: AnyTransaction)
}

And can be registered to a store by calling the register(middleware:) method.

store.register(middleware: MyMiddleware())

πš‚πšŽπš›πš’πšŠπš•πš’πš£πšŠπšπš’πš˜πš— πšŠπš—πš π™³πš’πšπšπš’πš—πš

TL;DR

struct MySerializableModel: SerializableModelType {
var count = 0
var label = "Foo"
var nullableLabel: String? = "Bar"
var nested = Nested()
var array: [Nested] = [Nested(), Nested()]
  struct Nested: Codable {
  var label = "Nested struct"
  }
}

let store = SerializableStore(model: TestModel(), diffing: .async)
store.$lastTransactionDiff.sink { diff in
  // diff is a `TransactionDiff` obj containing all of the changes that the last transaction has applied to the store's model.
}

A quick look at the TransactionDiff interface:

public struct TransactionDiff {
  /// The set of (`path`, `value`) that has been **added**/**removed**/**changed**.
  ///
  /// e.g. ``` {
  ///   user/name: <added β‡’ "John">,
  ///   user/lastname: <removed>,
  ///   tokens/1:  <changed β‡’ "Bar">,
  /// } ```
  public let diffs: [FlatEncoding.KeyPath: PropertyDiff]
  /// The identifier of the transaction that caused this change.
  public let transactionId: String
  /// The action that caused this change.
  public let actionId: String
  /// Reference to the transaction that cause this change.
  public var transaction: AnyTransaction
  /// Returns the `diffs` map encoded as **JSON** data.
  public var json: Data 
}

/// Represent a property change.
/// A change can be an **addition**, a **removal** or a **value change**.
public enum PropertyDiff {
  case added(new: Codable?)
  case changed(old: Codable?, new: Codable?)
  case removed
}

Using a SerializableModelType improves debuggability thanks to the console output for every transaction. e.g.

β–© INFO (-LnpwxkPuE3t1YNCPjjD) UPDATE_LABEL [0.045134 ms]
β–© DIFF (-LnpwxkPuE3t1YNCPjjD) UPDATE_LABEL {
    Β· label: <changed β‡’ (old: Foo, new: Bar)>, 
    Β· nested/label: <changed β‡’ (old: Nested struct, new: Bar)>, 
    Β· nullableLabel: <removed>
  }

π™°πšπšŸπšŠπš—πšŒπšŽπš

Dispatch takes advantage of Operations and OperationQueues and you can define complex dependencies between the operations that are going to be run on your store.

π™²πš‘πšŠπš’πš—πš’πš—πš πšŠπšŒπšπš’πš˜πš—πšœ

store.run(actions: [
  CounterAction.increase(amount: 1),
  CounterAction.increase(amount: 1),
  CounterAction.increase(amount: 1),
]) { context in
  // Will be executed after all of the transactions are completed.
}

Actions can also be executed in a synchronous fashion.

store.run(action: CounterAction.increase(amount: 1), strategy: .mainThread)
store.run(action: CounterAction.increase(amount: 1), strategy: .sync)

π™²πš˜πš–πš™πš•πšŽπš‘ π™³πšŽπš™πšŽπš—πšπšŽπš—πšŒπš’ π™Άπš›πšŠπš™πš‘

You can form a dependency graph by manually constructing your transactions and use the depend(on:) method.

let t1 = store.transaction(.addItem(cost: 125))
let t2 = store.transaction(.checkout)
let t3 = store.transaction(.showOrdern)
t2.depend(on: [t1])
t3.depend(on: [t2])
[t1, t2, t3].run()

πšƒπš›πšŠπšŒπš”πš’πš—πš 𝚊 πšπš›πšŠπš—πšœπšŠπšŒπšπš’πš˜πš— 𝚜𝚝𝚊𝚝𝚎

Sometimes it's useful to track the state of a transaction (it might be useful to update the UI state to reflect that).

store.run(action: CounterAction.increase(amount: 1)).$state.sink { state in
  switch(state) {
  case .pending: ...
  case .started: ...
  case .completed: ...
  }
}

π™³πšŽπšŠπš•πš’πš—πš πš πš’πšπš‘ πšŽπš›πš›πš˜πš›πšœ

struct IncreaseAction: ActionType {
  let count: Int
  
  func reduce(context: TransactionContext<Store<Counter>, Self>) {
    // Remember to always call `fulfill` to signal the completion of this operation.
    defer { context.fulfill() }
    // The operation terminates here because an error has been raised in this dispatch group.
    guard !context.rejectOnGroupError() { else return }
    // Kill the transaction and set TransactionGroupError.lastError.
    guard store.model.count != 42 { context.reject(error: Error("Max count reach") }
    // Business as usual...
    context.reduceModel { $0.count += 1 }
  }
}

π™²πšŠπš—πšŒπšŽπš•πš•πšŠπšπš’πš˜πš—

store.run(action: CounterAction.increase(amount: 1))
Dispatcher.main.cancelAllTransactions()

// or with a custom queue.
let queueId = "myCancellableQueue"
Dispatcher.main.registerQueue(id: queueId, queue: OperationQueue())
store.run(action: CounterAction.increase(amount: 1), mode: .async(queueId))
Dispatcher.main.cancelAllTransactions(id: queueId)
β–© π™„π™‰π™π™Š (-Lo4riSWZ3m5v1AvhgOb) INCREASE [βœ– canceled]

About

Unidirectional, transactional, operation-based Store implementation.

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Swift 99.9%
  • Shell 0.1%