Swiftpack.co - Package - bq/mini-swift

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 applications. 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

Requirements

  • Xcode 10 or later
  • Swift 5.0 or later
  • iOS 11 or later
  • macOS 10.13 or later
  • tvOS 11 or later

Installation

Swift Package Manager

  • Create a Package.swift file.
// swift-tools-version:5.0

import PackageDescription

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

Cocoapods

  • Add this to you Podfile:
pod "Mini-Swift"
  • We also offer two subpecs for logging and testing:
pod "Mini-Swift/Log"
pod "Mini-Swift/Test"

Usage

  • MiniSwift is a library which aims the ease of the usage of a Flux oriented architecture for Swift applications. Due its Flux-based nature, it heavily relies on some of its concepts like Store, State, Dispatcher, Action, Task and Reducer.

Architecture

State

  • The minimal unit of the architecture is based on the idea of the State. State is, as its name says, the representation of a part of the application in a moment of time.

  • The State is a simple struct which is conformed of different Tasks and different pieces of data that are potentially fulfilled by the execution of those tasks.

  • For example:

struct MyCoolState: State {
    let cool: Bool?
    let coolTask: Task

    init(cool: Bool = nil,
         coolTask: Task = Task()
        ) {
        self.cool = cool
        self.coolTask = coolTask
    }

    // Conform to State protocol
    func isEqual(to other: State) -> Bool {
        guard let state = other as? MyCoolState else { return false }
        return self.cool == state.cool && self.coolTask == state.coolState
    }
}
  • The core idea of a State is its immutability, so once created, no third-party objects are able to mutate it out of the control of the architecture flow.

  • As can be seen in the example, a State has a pair of Task + Result usually (that can be any object, if any), which is related with the execution of the Task. In the example above, CoolTask is responsible, through its Reducer to fulfill the Action with the Task result and furthermore, the new State.

Action

  • An Action is the piece of information that is being dispatched through the architecture. Any class can conform to the Action protocol, with the only requirement of being unique its name per application.
class RequestContactsAccess: Action {
  // As simple as this is.
}
  • Actions are free of have some pieces of information attached to them, that's why Mini provides the user with two main utility protocols: CompletableAction, EmptyAction and KeyedPayloadAction.

    • A CompletableAction is a specialization of the Action protocol, which allows the user attach both a Task and some kind of object that gets fulfilled when the Task succeeds.
    class RequestContactsAccessResult: CompletableAction {
    
      let requestContactsAccessTask: Task
      let grantedAccess: Bool?
    
      typealias Payload = Bool
    
      required init(task: Task, payload: Payload?) {
          self.requestContactsAccessTask = task
          self.grantedAccess = payload
      }
    }
    
    • An EmptyAction is a specialization of CompletableAction where the Payload is a Swift.Never, this means it only has associated a Task.
    class ActivateVoucherLoaded: EmptyAction {
    
      let activateVoucherTask: Task
    
      required init(task: Task) {
          self.activateVoucherTask = task
      }
    }
    
    • A KeyedPayloadAction, adds a Key (which is Hashable) to the CompletableAction. This is a special case where the same Action produces results that can be grouped together, tipically, under a Dictionary (i.e., an Action to search contacts, and grouped by their main phone number).
    class RequestContactLoadedAction: KeyedCompletableAction {
    
      typealias Payload = CNContact
      typealias Key = String
    
      let requestContactTask: Task
      let contact: CNContact?
      let phoneNumber: String
    
      required init(task: Task, payload: CNContact?, key: String) {
          self.requestContactTask = task
          self.contact = payload
          self.phoneNumber = key
      }
    }
    

Store

  • A Store is the hub where decissions and side-efects are made through the ingoing and outgoing Actions. A Store is a generic class to inherit from and associate a State for it.

  • A Store may produce State changes that can be observed like any other RxSwift's Observable. In this way a View, or any other object of your choice, can receive new States produced by a certain Store.

  • A Store reduces the flow of a certain amount of Actions through the var reducerGroup: ReducerGroup property.

  • The Store is implemented in a way that has two generic requirements, a State: StateType and a StoreController: Disposable. The StoreController is usually a class that contains the logic to perform the Actions that might be intercepted by the store, i.e, a group of URL requests, perform a database query, etc.

  • Through generic specialization, the reducerGroup variable can be rewritten for each case of pair State and StoreController without the need of subclassing the Store.

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

    var reducerGroup: ReducerGroup {
        return ReducerGroup { [
            Reducer(of: OneTestAction.self, on: self.dispatcher) { action in
                self.state = self.state.copy(testTask: *.requestSuccess(), counter: *action.counter)
            }
        ] }
    }
}
  • In the snippet above, we have a complete example of how a Store would work. We use the ReducerGroup to indicate how the Store will intercept Actions of type OneTestAction and that everytime it gets intercepted, the Store's State gets copied (is not black magic 🧙‍, is through a set of Sourcery scripts that are distributed with this package).

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.

  • When working with Store instances, you may retain a strong reference of its reducerGroup, this is done using the subscribe() method, which is a Disposable that can be used like below:
var bag = DisposeBag()
let store = Store<TestState, TestStoreController>(TestState(), dispatcher: dispatcher, storeController: TestStoreController())
store
    .subscribe()
    .disposed(by: bag)

Dispatcher

  • The last piece of the architecture is the Dispatcher. In an application scope, there should be only one Dispatcher alive from which every action is being dispatched.
let action = TestAction()
dispatcher.dispatch(action, mode: .sync)
  • With one line, we can notify every Store which has defined a reducer for that type of Action.

Authors & Collaborators

License

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

Github

link
Stars: 23
Help us keep the lights on

Used By

Total: 0

Releases

v1.1.0 - Sep 11, 2019

Added SingeTrait and CompletableTrait extensions for CompletableAction, KeyedCompletableAction and EmptyAction.

v1.0.2 - Sep 10, 2019

More issues with accessibility levels (thank you @EdiLT)

v1.0.1 - Sep 10, 2019

This release includes the stable and production-ready version of Mini, which also fixes a bug in the accessibility level on the ReducerGroup initializer (thanks to @EdiLT).

v1.0.0 - Sep 5, 2019

First major release 🙌

v0.3.2 - Aug 20, 2019

First production-ready release! 🚀