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.
- Xcode 10 or later
- Swift 5.0 or later
- iOS 11 or later
- macOS 10.13 or later
- tvOS 11 or later
- Add this to you
Cartfile:
github "masmovil/masmini-swift"
- Add this to you
Podfile:
pod "MasMini-Swift"
- We also offer two subpecs for logging and testing:
pod "MasMini-Swift/Log"
pod "MasMini-Swift/Test"
- MasMiniSwift 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.
-
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
structwhich 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
Stateis 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
Statehas a pair ofTask+Resultusually (that can be any object, if any), which is related with the execution of theTask. In the example above,CoolTaskis responsible, through itsReducerto fulfill theActionwith theTaskresult and furthermore, the newState.
- An
Actionis the piece of information that is being dispatched through the architecture. Anyclasscan conform to theActionprotocol, 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,EmptyActionandKeyedPayloadAction.- A
CompletableActionis a specialization of theActionprotocol, which allows the user attach both aTaskand some kind of object that gets fulfilled when theTasksucceeds.
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
EmptyActionis a specialization ofCompletableActionwhere thePayloadis aSwift.Never, this means it only has associated aTask.
class ActivateVoucherLoaded: EmptyAction { let activateVoucherTask: Task required init(task: Task) { self.activateVoucherTask = task } }
- A
KeyedPayloadAction, adds aKey(which isHashable) to theCompletableAction. This is a special case where the sameActionproduces results that can be grouped together, tipically, under aDictionary(i.e., anActionto 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 } }
- A
-
A
Storeis the hub where decissions and side-efects are made through the ingoing and outgoingActions. AStoreis a generic class to inherit from and associate aStatefor it. -
A
Storemay produceStatechanges that can be observed like any other RxSwift'sObservable. In this way aView, or any other object of your choice, can receive newStates produced by a certainStore. -
A
Storereduces the flow of a certain amount ofActions through thevar reducerGroup: ReducerGroupproperty. -
The
Storeis implemented in a way that has two generic requirements, aState: StateTypeand aStoreController: Disposable. TheStoreControlleris usually a class that contains the logic to perform theActionsthat might be intercepted by the store, i.e, a group of URL requests, perform a database query, etc. -
Through generic specialization, the
reducerGroupvariable can be rewritten for each case of pairStateandStoreControllerwithout the need of subclassing theStore.
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
Storewould work. We use theReducerGroupto indicate how theStorewill interceptActions of typeOneTestActionand that everytime it gets intercepted, theStore'sStategets 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
Sourceryin your project and use the templates that can be downloaded directly from the repository under theTemplatesdirectory.
- When working with
Storeinstances, you may retain a strong reference of itsreducerGroup, this is done using thesubscribe()method, which is aDisposablethat can be used like below:
var bag = DisposeBag()
let store = Store<TestState, TestStoreController>(TestState(), dispatcher: dispatcher, storeController: TestStoreController())
store
.subscribe()
.disposed(by: bag)- The last piece of the architecture is the
Dispatcher. In an application scope, there should be only oneDispatcheralive from which every action is being dispatched.
let action = TestAction()
dispatcher.dispatch(action, mode: .sync)- With one line, we can notify every
Storewhich has defined a reducer for that type ofAction.
- Edilberto Lopez Torregrosa
- Raúl Pedraza León
- Jorge Revuelta
- Francisco GarcĆa Sierra
- Pablo Orgaz
- SebastiƔn Varela
Mini-Swift is available under the Apache 2.0. See the LICENSE file for more info.
