CocoaPods trunk is moving to be read-only. Read more on the blog, there are 18 months to go.

Swivel 0.1.3

Swivel 0.1.3

Maintained by Akkyie.



Swivel 0.1.3

  • By
  • Akio Yasui

Swivel

Build Status Coverage Status

Yet another architecture framework.

Inspired from Elm and Redux. Optimized for Swift.

Features

  • Swifty interfaces: type-safe and conprehensive
  • Fancy and efficient value observation with Swift 4 KeyPaths
  • Architectural separation of asynchronicity and side effects with Effects
  • Accessing from multiple threads are supported (not yet fully tested)
  • No specific Reactive framework dependency (Reactive adapters for RxSwift/ReactiveSwift are coming!)

Installation

⚠️ Swivel is still in very early stage of development. Use it carefully.

Carthage

github "akkyie/Swivel" ~> 0.1

CocoaPods

pod "Swivel", "~> 0.1"

SwiftPM

.package(url: "https://github.com/akkyie/Swivel.git", from: "0.1"),

Concepts

State describes a state. Think you are implementing a counter:

struct CounterState {
  var count: Int
}

A counter should be incremented, and sometimes decremented. You can enumerate how the state is updated, as Message:

enum CounterMessage: Message {
    case increment(amount: Int)
    case decrement(amount: Int)
}

And now you can describe how a CounterState is updated when it receives a message, to make it conform to State protocol.

struct CounterState: State {
    var count: Int = 0

    static func update(_ state: inout CounterState, with message: Message) -> Effect? {
        guard let message = message as? CounterMessage else { return nil }

        switch message {
        case let .increment(amount):
            state.count += amount
            return nil

        case let .decrement(amount):
            state.count -= amount
            return nil
        }
    }
}

States in Swivel can be described as Model+Update in Elm, or State+Reducer in Redux.

Messages are apparently Msg in Elm, or Action in Redux.

The update function of a state should be pure (similarly to Elm's updates and Redux's reducers.) You should not do anything asynchronous, e.g. API calls or database accesses over the network. Use Effect, a concept borrowed from Elm's Command, to make these side effects.

Subscription

You can watch for changes of a State with subscribe method of the Store.

By default, the closures get called right after subscription, and everytime messages are dispatched, even when the state is not changed:

let store = Store(initialState: CounterState(count: 0))

let unsubscribe = store.subscribe(\CounterState.count) { count in
    print("Count: \(count)")
}

store.dispatch(CounterMessage.increment(1))
store.dispatch(CounterMessage.increment(2))
store.dispatch(CounterMessage.increment(0))
store.dispatch(CounterMessage.increment(0))

// Count: 0
// Count: 1
// Count: 3
// Count: 3
// Count: 3

You can change this behavior by passing immediately: false or skipRepeats: true:

let store = Store(initialState: CounterState(count: 0))

let unsubscribe = store.subscribe(\CounterState.count, immediately: false, skipRepeats: true) { count in
    print("Count: \(count)")
}

store.dispatch(CounterMessage.increment(1))
store.dispatch(CounterMessage.increment(2))
store.dispatch(CounterMessage.increment(0))
store.dispatch(CounterMessage.increment(0))

// Count: 1
// Count: 3

You can specify skipRepeats: true only when the subscribed value (\CounterState.count in the example above) is Equatable.

Asynchronicity and Side Effects

Effect is what a state's update function returns and describes the procedure to asynchronous dispatch or any side effects. Example:

enum CounterEffect {
    static func requestAndIncrement() -> Effect {
        return { dispatch in
            request("https://pastebin.com/raw/uQ8LH0Gr") { (result: Int) in
                dispatch(CounterMessage.increment(amount: result))
            }
        }
    }
}

struct CounterState: State {
    var count: Int = 0
    var isLoading: Bool = false

    static func update(state: inout CounterState, message: Message) -> Effect? {
        guard let message = message as? CounterMessage else { return nil }

        switch message {
        case let .increment(amount):
            state.count += amount
            state.isLoading = false
            return nil

        case let .requestAndIncrement():
            state.isLoading = true
            return CounterEffect.requestAndIncrement() // <-- Returning an Effect
        }
    }
}

Separation of concerns: Substates and composition of updates

Your states may have other states as it's property in order to divide a state into a number of substates:

struct AppState: State {
    var user: UserState
    var auth: AuthState
    var document: DocumentState

    static func update(_ state: inout AppState, with message: Message) -> Effect? {
        let update = makeSerialUpdate(\.user, \.auth, \.document)
        return update(state, message)
    }
}

makeSerialUpdate(states: State...) constructs a new update function which updates given substates serially with a message. Effects returned from each states are called after all state changes are made.