Cycler 3.0.1

Cycler 3.0.1

Maintained by muukii, muukii.



 
Depends on:
RxSwift~> 4.2.0
RxCocoa~> 4.2.0
 

Cycler 3.0.1

Cycler

This readme is working in progress

What is Cycler?

This is one of the ideas for ViewModel layer.

Mainly it's inspired by Flux architecture.

CyclerType

CyclerType just defines clean data-flow.

So, We are free that how we use Cycler.

One of the usages, CyclerType adapts ViewModel of MVVM architecture.

It has State that is observable

It updates State by receiving Mutation or Action

Receive Mutation as Commit

Receive Action as Dispatch

It emits Activity by receiving Mutation or Action

Sometimes, There are some events that don't need store to State by Action or Mutation. So, We call them Activity.

Protocol

public protocol CyclerType {
  associatedtype State
  associatedtype Activity
  var state: Storage<State> { get }
}

Extension-Methods

extension CyclerType {

  public var activity: Signal<Activity>

  public func commit(
      _ name: String = "",
      _ description: String = "",
      file: StaticString = #file,
      function: StaticString = #function,
      line: UInt = #line,
      _ mutate: @escaping (MutableStorage<State>) throws -> Void
      ) rethrows

  public func dispatch<T>(
    _ name: String = "",
    _ description: String = "",
    file: StaticString = #file,
    function: StaticString = #function,
    line: UInt = #line,
    _ action: (CyclerWeakContext<Self>) throws -> T
    ) rethrows -> T
}

Storage

This is Storage that used for current State.

public class Storage<T> {
  public var value: T { get }
  public func add(subscriber: @escaping (T) -> Void) -> Token
  public func remove(subscriber: Token)
}

public final class MutableStorage<T> : Storage<T> {
  public func replace(_ value: T)
  public func batchUpdate(_ update: (MutableStorage<T>) -> Void)
  public func update<E>(_ value: E, _ keyPath: WritableKeyPath<T, E>)
  public func updateIfChanged<E>(_ value: E, _ keyPath: WritableKeyPath<T, E>, comparer: (E, E) -> Bool)
  public func asStorage() -> Storage<T>
}

It can use without RxSwift

We can use Storage as standalone.

Logging

We can log event about Cycler.

  • Logs are
    • Changes state
    • Receives mutations
    • Receives actions
    • Emits activities
public protocol MutableStorageLogging {

  func didChange(value: Any, for keyPath: AnyKeyPath, root: Any)
  func didReplace(root: Any)
}

public protocol CycleLogging : MutableStorageLogging {

  func didEmit(activity: Any, file: StaticString, function: StaticString, line: UInt, on cycler: AnyCyclerType)
  func willDispatch(name: String, description: String, file: StaticString, function: StaticString, line: UInt, on cycler: AnyCyclerType)
  func willMutate(name: String, description: String, file: StaticString, function: StaticString, line: UInt, on cycler: AnyCyclerType)
  func didMutate(name: String, description: String, file: StaticString, function: StaticString, line: UInt, on cycler: AnyCyclerType)
}

Usage

Actual Code

Define ViewModel

import Cycler

class ViewModel : CyclerType {

  enum Activity {
    case didReachBigNumber
  }

  struct State {

    // - Data
    fileprivate var count: Int = 0

    // - Computed
    // It will be subscribed whole of this State, so, we can use computed property.
    var countText: String {
      return count.description
    }
  }

  private let disposeBag = DisposeBag()

  let state: Storage<State> = .init(.init(count: 0))

  init() {

  }

  func increment(number: Int) {

    // Dispatch Action
    // Action can contain async operation.
    dispatch("increment") { (context) in

      // Context references self weakly.

      Observable.just(())
        .delay(0.1, scheduler: MainScheduler.instance)
        .do(onNext: {

          // Retain references of context
          // So, run operation completely in this scope.
          context.retain { c in

            // Mutation
            // Transaction for mutating.
            c.commit { (state) in
              // State is MutableStorage.
              state.updateIfChanged(state.value.count + number, \.count)
            }

            if c.currentState.count > 10 {
              // Emit Activity.
              // Activity just an event that does not need to store to State.
              c.emit(.didReachBigNumber)
            }
          }
        })
        .subscribe()
      }
      .disposed(by: disposeBag)
  }

  func decrement(number: Int) {

    dispatch("decrement") { _ in
      commit { (state) in
        state.updateIfChanged(state.value.count - number, \.count)
      }
    }

  }
}

Subscribe State

let viewModel = ViewModel()

// Subscribe one property of the State.

viewModel
  .state
  .asObservable()
  .map { $0.countText }
  .distinctUntilChanged()

// Or, subscribe one property of the State by KeyPath.

viewModel
  .state
  .asObservable(keyPath: \.countText)
  .distinctUntilChanged()

// Or,

viewModel
  .state
  .changed(\.countText) // This includes `distinctUntilChanged`

distinctUntilChanged is very important.

Mutation will mutate whole of the State. Observable from the State will send event whenever updating the State. This behavior will cause unnecessary operations.

Subscribe Activity

// Subscribe activity.
viewModel
  .activity
  .emit(...)

Basically Demo