Skip to content

wircho/Swux

Repository files navigation

Swux - A Swiftier Redux

Swux is a Swiftier implementation of Redux inspired by ReSwift.

Read the Medium post here.

Table of Contents

Installation

Cocoapods

Add this to your Podfile:

pod 'Swux', :git => 'https://github.com/wircho/Swux.git'

Carthage

Add this to your Cartfile:

github "wircho/Swux"

Make sure to specify the platform (iOS or MacOS) when updating the Carthage builds. Use either carthage update --platform ios or carthage update --platform macos accordingly.

Usage

Step 1: Define your application state

The application state is a structure that should uniquely define the state of your app's UI at any point in time. For example, an app that displays a single integer counter could have the following state:

import Swux

struct AppState {
  var counter: Int
}

Step 2: Define actions and implement their mutators

The AppState above may be mutated by incrementing or decrementing the counter, for example, so we could define those actions as follows:

struct IncrementCounter: ActionProtocol {
  func mutate(_ state: inout AppState) { state.counter += 1 }
}

struct DecrementCounter: ActionProtocol {
  func mutate(_ state: inout AppState) { state.counter -= 1 }
}

You could also add an action that sets the counter to a specific value, for example:

struct SetCounter: ActionProtocol {
  let value: Int
  func mutate(_ state: inout AppState) { state.counter = value }
}

Step 3: Initialize your store

The store is responsible for storing the application state and queuing up actions. You must initialize it with an initial state:

let store = Store(AppState(counter: 0))

Step 4: Dispatch actions to the store and receive state updates

You may submit actions to the store using the dispatch method as follows:

store.dispatch(IncrementCounter())
store.dispatch(SetCounter(value: 5))

You can subscribe objects implementing SubscriberProtocol to receive state change updates from the stateChanged(to newState: AppState) method. Use let disposable = store.subscribe(self) and retain the returned Disposable object until you no longer need to listen to state updates. From a view controller you could do:

class ViewController: UIViewController, SubscriberProtocol {
    var disposable: Disposable?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        disposable = store.subscribe(self)
    }
    
    func stateChanged(to newState: AppState) {
        /* Use newState to update the UI */
    }
}

Best Practices

Optional States

If your application state is optional, for example,

struct AppState {
  /* some properties */
}

let store = Store<AppState?>(nil)

you may dispatch actions whose state type is the unwrapped type AppState. These actions' mutators are only performed internally when the state is not nil.

Enum States

Because of Swift's excluse ownership feature and copy-on-write types, you may be able to completely avoid copying your state by always performing mutations in place and avoiding multiple copies of the same structure. This is difficult to avoid for enum states with associated types, since Swift does not currently provide in-place mutation of enum types. One solution is to temporarily erase values while you mutate their local copies. You may conveniently define and extend one helper protocol for each enum case that has associated values. The following example illustrates this technique:

/* STATE */

enum AppState {
  case uninitialized
  case point(CGPoint)
  case segment(CGPoint, CGPoint)
}

/* HELPER PROTOCOLS */

protocol PointActionProtocol: ActionProtocol where State == AppState {
  func mutatePoint(_ point: inout CGPoint)
}

extension PointActionProtocol {
  func mutate(_ state: inout AppState) {
    switch state {
    case .point(var point):
      state = .uninitialized
      mutatePoint(point)
      state = .point(point)
    default: return
    }
  }
}

protocol SegmentActionProtocol: ActionProtocol where State == AppState {
  func mutateSegment(_ first: inout CGPoint, _ second: inout CGPoint)
}

extension SegmentActionProtocol {
  func mutate(_ state: inout AppState) {
    switch state {
    case var .segment(first, second):
      state = .uninitialized
      mutateSegment(first, second)
      state = .segment(first, second)
    default: return
    }
  }
}

/* USAGE */

struct MovePoint: PointActionProtocol {
  let by: CGVector
  func mutatePoint(_ point: inout CGPoint) {
    point.x += by.dx
    point.y += by.dy
  }
}

struct MoveSegment: SegmentActionProtocol {
  let by: CGVector
  func mutateSegment(_ first: inout CGPoint, _ second: inout CGPoint) {
    first.x += by.dx
    first.y += by.dy
    second.x += by.dx
    second.y += by.dy
  }
}

Sync/Async Subscribers And Dispatch

Async Subscribers

When you subscribe to the store's state updates, you may specify an optional DispatchQueue as follows:

disposable = store.subscribe(self, on: DispatchQueue.global(qos: .background))

This way, the stateChanged method is asynchronously dispatched to the specified queue. If you do not speficy a queue, the stateChanged method is called on the main thread.

Async Dispatch

You may dispatch actions asynchronously (to the store's action queue) by specifying a DispatchMode as follows:

store.dispatch(SomeAction(), dispatchMode: .async)

By default all actions are performed synchronously.