Mini-Swift

The minimal expression of a Flux architecture in Swift.

Mini is built with be a first class citizen in Swift applications: macOS, iOS and tvOS. With Mini, you can create a thread-safe application with a predictable unidirectional data flow, focusing on what really matters: build awesome applications.

Release Version Release Date Pod Platform GitHub

Build Status codecov Documentation

Requirements

Installation

Swift Package Manager

// swift-tools-version:5.0

import PackageDescription

let package = Package(
  name: "MiniSwiftProject",
  dependencies: [
    .package(url: "https://github.com/minuscorp/Mini-Swift.git"),
  ],
  targets: [
    .target(name: "MiniSwiftProject", dependencies: ["Mini" /*, "MiniPromises, MiniTasks"*/])
  ]
)
$ swift build

Cocoapods

pod "Mini-Swift"
# pod "Mini-Swift/MiniPromises"
# pod "Mini-Swift/MiniTasks"

Usage

Architecture

State

// If you're using MiniPromises
struct MyCoolState: StateType {
    let cool: Promise<Bool>
}

// If you're using MiniTasks
struct MyCoolState: StateType {
    let cool: Bool?
    let coolTask: AnyTask
}
let promise: Promise<Bool> = .idle()
promise.date = Date()
// Later on...
let date: Date = promise.date

let task: AnyTask = .idle()
task.date = Date()
// Later on...
let date: Date = task.date

Action

struct RequestContactsAccess: Action {
  // As simple as this is.
}

We take the advantage of using struct, so all initializers are automatically synthesized.

Examples are done with Promise, but there’re equivalent to be used with Tasks.

Store

extension Store where State == TestState, StoreController == TestStoreController {

    var reducerGroup: ReducerGroup {
        return ReducerGroup(
            // Using Promises
            Reducer(of: OneTestAction.self, on: dispatcher) { action in
                self.state = self.state.copy(testPromise: *.value(action.counter))
            },
            // Using Tasks
            Reducer(of: OneTestAction.self, on: dispatcher) { action in
                self.state = self.state.copy(data: *action.payload, dataTask: *action.task)
            }
        )
    }
}

If you are using SPM or Carthage, they doesn’t really allow to distribute assets with the library, in that regard we recommend to just install Sourcery in your project and use the templates that can be downloaded directly from the repository under the Templates directory.

let bag = DisposeBag()
let store = Store<TestState, TestStoreController>(TestState(), dispatcher: dispatcher, storeController: TestStoreController())
store
    .subscribe()
    .disposed(by: bag)

Dispatcher

let action = TestAction()
dispatcher.dispatch(action, mode: .sync)

Advanced usage

Using Promises

// We define our state in first place:
struct TestState: StateType {
    // Our state is defined over the Promise of an Integer type.
    let counter: Promise<Int>

    init(counter: Promise<Int> = .idle()) {
        self.counter = counter
    }

    public func isEqual(to other: StateType) -> Bool {
        guard let state = other as? TestState else { return false }
        guard counter == state.counter else { return false }
        return true
    }
}

// We define our actions, one of them represents the request of a change, the other one the response of that change requested.

// This is the request
struct SetCounterAction: Action {
    let counter: Int
}

// This is the response
struct SetCounterActionLoaded: Action {
    let counter: Int
}

// As you can see, both seems to be the same, same parameters, initializer, etc. But next, we define our StoreController.

// The StoreController define the side-effects that an Action might trigger.
class TestStoreController: Disposable {
    
    let dispatcher: Dispatcher
    
    init(dispatcher: Dispatcher) {
        self.dispatcher = dispatcher
    }
    
    // This function dispatches (always in a async mode) the result of the operation, just giving out the number to the dispatcher.
    func counter(_ number: Int) {
        self.dispatcher.dispatch(SetCounterActionLoaded(counter: number), mode: .async)
    }
    
    public func dispose() {
        // NO-OP
    }
}

// Last, but not least, the Store definition with the Reducers
extension Store where State == TestState, StoreController == TestStoreController {

    var reducerGroup: ReducerGroup {
        ReducerGroup(
            // We can use Promises:
            // We set the state with a Promise as .pending, someone has to fill the requirement later on. This represents the Request.
            Reducer(of: SetCounterAction.self, on: self.dispatcher) { action in
                guard !self.state.counter.isOnProgress else { return }
                self.state = TestState(counter: .pending())
                self.storeController.counter(action.counter)
            },
            // Next we receive the Action dispatched by the StoreController with a result, we must fulfill our Promise and notify the store for the State change. This represents the Response.
            Reducer(of: SetCounterActionLoaded.self, on: self.dispatcher) { action in
                self.state.counter
                    .fulfill(action.counter)
                    .notify(to: self)
            }
        )
    }
}

Using Tasks

// We define our state in first place:
struct TestState: StateType {
    // Our state is defined over the Promise of an Integer type.
    let counter: Int?
    let counterTask: AnyTask

    init(counter: Int = nil,
         counterTask: AnyTask = .idle()) {
        self.counter = counter
        self.counterTask = counterTask
    }

    public func isEqual(to other: StateType) -> Bool {
        guard let state = other as? TestState else { return false }
        guard counter == state.counter else { return false }
        guard counterTask == state.counterTask else { return false }
        return true
    }
}

// We define our actions, one of them represents the request of a change, the other one the response of that change requested.

// This is the request
struct SetCounterAction: Action {
    let counter: Int
}

// This is the response
struct SetCounterActionLoaded: Action {
    let counter: Int
    let counterTask: AnyTask
}

// As you can see, both seems to be the same, same parameters, initializer, etc. But next, we define our StoreController.

// The StoreController define the side-effects that an Action might trigger.
class TestStoreController: Disposable {
    
    let dispatcher: Dispatcher
    
    init(dispatcher: Dispatcher) {
        self.dispatcher = dispatcher
    }
    
    // This function dispatches (always in a async mode) the result of the operation, just giving out the number to the dispatcher.
    func counter(_ number: Int) {
        self.dispatcher.dispatch(
            SetCounterActionLoaded(counter: number, 
            counterTask: .success()
            ),
            mode: .async)
    }
    
    public func dispose() {
        // NO-OP
    }
}

// Last, but not least, the Store definition with the Reducers
extension Store where State == TestState, StoreController == TestStoreController {

    var reducerGroup: ReducerGroup {
        ReducerGroup(
            // We can use Tasks:
            // We set the state with a Task as .running, someone has to fill the requirement later on. This represents the Request.
            Reducer(of: SetCounterAction.self, on: dispatcher) { action in
                guard !self.state.counterTask.isRunning else { return }
                self.state = TestState(counterTask: .running())
                self.storeController.counter(action.counter)
            },
            // Next we receive the Action dispatched by the StoreController with a result, we must fulfill our Task and update the data associated with the execution of it on the State. This represents the Response.
            Reducer(of: SetCounterActionLoaded.self, on: dispatcher) { action in
                guard self.state.rawCounterTask.isRunning else { return }
                self.state = TestState(counter: action.counter, counterTask: action.counterTask)
            }
        )
    }
}

Documentation

All the documentation available can be found here

Maintainers

Authors & Collaborators

License

Mini-Swift is available under the Apache 2.0. See the LICENSE file for more info.