Latest 0.1.3
Homepage https://github.com/akkyie/Swivel
License MIT
Platforms ios 8.0, osx 10.9
Authors

Build Status Coverage Status CocoaPods Carthage

Yet another architecture framework.

Inspired from Elm and Redux. Optimized for Swift.

Features

  • Swifty interfaces: type-safe and comprehensive
  • 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 a very early stage of development. Use it carefully.

Carthage

github "akkyie/Swivel" ~> 0.1

CocoaPods

pod "Swivel", "~> 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 sub-states:

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 sub-states serially with a message. Effects returned from each state are called after every change on the state are made.

Latest podspec

{
    "name": "Swivel",
    "version": "0.1.3",
    "summary": "Yet another architecture framework for Swift",
    "description": "Yet another architecture framework.nInspired from Elm and Redux, optimized for Swift.",
    "homepage": "https://github.com/akkyie/Swivel",
    "license": {
        "type": "MIT",
        "file": "LICENSE"
    },
    "authors": {
        "Akio Yasui": "[email protected]"
    },
    "platforms": {
        "ios": "8.0",
        "osx": "10.9"
    },
    "source": {
        "git": "https://github.com/akkyie/Swivel.git",
        "tag": "0.1.3"
    },
    "source_files": "Sources/Swivel"
}

Pin It on Pinterest

Share This