UDF (Unidirectional Data Flow) is a library based on Unidirectional Data Flow pattern. It lets you build maintainable, testable, and scalable apps.
A unidirectional data flow is a design pattern where a state (data) flows down and events (actions) flow up. It's important that UI never edits or sends back data. That's why UI is usually provided with immutable data. It allows having a single source of truth for a whole app and effectively separates domain logic from UI.
Unidirectional Data Flow design pattern has been popular for a long time in the web development. Now it's time for the mobile development. Having started from multi-platform solutions like React Native and Flutter, Unidirectional Data Flow now becomes a part of native. SwiftUI for Swift and Jetpack Compose for Kotlin are implemented based on ideas of UDF. That's why we in inDriver decided to develop our own UDF library for our purposes.
Here are the main advantages of this UDF implementation:
Differences from other popular UDF implementations:
RxFeedback - requires RxSwift
The Composable Architecture - iOS13+ only because of Combine
ReSwift - no instruments for modularization
Let's imagine a simple counter app. It shows a counter label and two buttons "+" and "-" to increment and decrement the counter. Let's consider the stages of its creation.
Firstly we need to declare the state of the app:
struct AppState: Equatable {
var counter = 0
}
State is all the data of an app. In our case, it's just an int counter. Next, we need to know when buttons are tapped:
enum CounterAction: Action {
case decrementButtonTapped
case incrementButtonTapped
}
We use an enum and an Action
protocol for that. Action describes all of the actions that can occur in your app.
Next, we need to update our state according to an action:
func counterReducer(state: inout AppState, action: Action) {
guard let action = action as? CounterAction else { return }
switch action {
case .decrementButtonTapped:
state.counter -= 1
case .incrementButtonTapped:
state.counter += 1
}
}
Reducer is a pure function that updates a state. Now we need to glue it together:
let store = Store<AppState>(state: .init(), reducer: counterReducer)
Store combines all the above things together.
Let's take a look at UI now:
class CounterViewController: UIViewController, ViewComponent {
typealias Props = Int
@IBOutlet var counterLabel: UILabel!
var disposer = Disposer()
var props: Int = 0 {
didSet {
guard isViewLoaded else { return }
counterLabel.text = "\(props)"
}
}
@IBAction func decreaseButtonTapped(_ sender: UIButton) {
store.dispatch(CounterAction.decrementButtonTapped)
}
@IBAction func increaseButtonTapped(_ sender: UIButton) {
store.dispatch(CounterAction.incrementButtonTapped)
}
}
CounterViewController
implements ViewComponent
protocol. It guarantees that a component receives a new state only if the state was changed and this process always occurs in the main thread. In CounterViewController
we declare props property and update UI in its didSet. Now we have to connect our ViewController to the store:
let counterViewController = CounterViewController()
counterViewController.connect(to: store, state: \.counter)
Notice that we can choose witch part of the state we want to observe.
Imagine that you would like to reuse your CounterViewController
in another app. Or you have a much bigger reusable feature with many View Controllers. In this case, your AppState will look like this:
struct AppState: Equatable {
var counter = 0
var bigFeature = BigFeature()
}
Obviously you don't want your features to know about AppState. You can easily decouple them by scope:
let store = Store<AppState>(state: .init(), reducer: counterReducer)
connectCounter(to: store.scope(\.counter))
connectBigFeature(to: store.scope(\.bigFeature))
...
//Somewhere in Counter.framework
func connectCounter(to store: Store<Int>) {
...
counterViewController.connect(to: store)
}
//Somewhere in BigFeature.framework
func connectCounter(to store: Store<BigFeature>) {
...
firstViewController.connect(to: store, state: \.first)
}
Now you can move your features to separate frameworks and use them wherever you want.
You can add the UDF to an Xcode project by adding it as a package dependency.
https://github.com/inDriver/UDF
The UDF idea is based on Alexey Demedetskiy ideas and MaximBazarov implementation of Unidirection Data Flow Pattern. Originally the UDF was a fork of Unicore.
The Composable Architecture inspired our implementation of a scope function and modularisation.
Also, we would like to thank all people that took part in development, testing, and using the UDF: Artem Lytkin, Ivan Dyagilev, Andrey Zamorshchikov, Dmitry Filippov, Eldar Adelshin, Anton Nochnoy, Denis Sidorenko, Ilya Kuznetsov and Viktor Gordienko.
If you have any questions or suggestions, please do not hesitate to contact Anton Goncharov or Yuri Trykov.
UDF is released under the Apache 2.0 license. See LICENSE for details.
Copyright 2021 Suol Innovations Ltd.
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.
link |
Stars: 55 |
Last commit: 1 week ago |
minor bug fixes
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics