The simplest architecture for FunctionalKit Inspired by RxFeedback, but it uses Monads (from FunctionalKit) instead of RxSwift, and allows dependency injection out of the box.
Docs: ArchitectureKit docs
static func pure(
initialState: State,
context: Context,
reducer: @escaping (State, Event) -> State,
uiBindings: [(State) -> ()],
userActions: [SystemUserAction],
feedback: [SystemFeedback]
) -> System {
This architectural approach, fits on the View layer of Clean Architecture. It is an alternative to Model-View-Presenter or Model-View-ViewModel, and it is strongly inspired by Redux.
The idea is to constrain the changes to view state in order to enforce correctness. Changes to state are explicity documented by Events and by a pure reducer function. This approach also allows testing presentation logic with ease (it also includes a mechanism to inject dependencies, such views, API Clients, etc.)
ArchitectureKit only contains FunctionalKit dependency
These are currently the supported options:
Add this to Cartfile
github "RPallas92/ArchitectureKit" "master"
$ carthage update
The purpose of this example is explain how to use FunctionalKit. It's a simple counter with an increment and decrement buttons. The State is just an integer that contains the current count.
class CounterViewController: UIViewController {
@IBOutlet weak var label: UILabel?
@IBOutlet weak var minus: UIButton?
@IBOutlet weak var plus: UIButton?
override func viewDidLoad() {
super.viewDidLoad()
typealias State = Int
typealias CounterSystem = System<State, Event, CounterError, Context>
typealias CounterUserAction = UserAction<State, Event, CounterError, Context>
typealias CounterFeedback = Feedback<State, Event, CounterError, Context>
enum Event {
case increment
case decrement
}
struct Context {
}
enum CounterError: Error {
case generic
}
let tapPlus = CounterUserAction.init(trigger: Event.increment)
let tapMinus = CounterUserAction.init(trigger: Event.decrement)
@IBAction func plusButtonTapped(_ sender: Any) {
tapPlus.execute()
}
@IBAction func minusButtonTapped(_ sender: Any) {
tapMinus.execute()
}
let bindUI:(State) ->() = { state in
self.label?.text = String(state)
}
CounterSystem.pure(
initialState: 0,
context: Context(),
reducer: { (state, event) -> State in
switch event {
case .increment:
return state + 1
case .decrement:
return state - 1
}
},
uiBindings: [bindUI],
userActions: [tapPlus,tapMinus],
feedback: []
)
}
}
You can find in this repository a simple but full application with:
- Dependency injection.
- Network requests.
- 2 screens.
- Unit testing.
In the releases page of the repository, you can see 2 different architecture approaches (one with ArchitectureKit and other with MVP).
-
Straightforward
- if it's state -> State
- if it's a way to modify state -> Event
- it it's an effect -> encode it into part of state and then design a feedback loop
-
Declarative
- System behavior is first declaratively specified and effects begin after subscribe is called => Compile time proof there are no "unhandled states"
-
Debugging is easier
- A lot of logic is just normal pure function that can be debugged using Xcode debugger, or just printing the commands.
-
Can be applied on any level
- Entire system
- application (state is stored inside a database, CoreData, Firebase, Realm)
- view controller (state is stored inside
system
operator) - inside feedback loop (another
system
operator inside feedback loop)
-
Works awesome with dependency injection
-
Testing
- Reducer is a pure function, just call it and assert results
- In case effects are being tested -> Expectations
-
Can model circular dependencies
-
Completely separates business logic from effects.
- Business logic can be transpiled between platforms (ShiftJS, C++, J2ObjC)
Copyright 2017 - 2018 Ricardo Pallás Román
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.