πΎ β»οΈ π Unidirectional Input / Output framework with Combine.
SwiftUI Playground | UIKit Playground |
---|---|
Ricemill represents unidirectional data flow with these components.
The rule of Input is having Subject properties that are defined internal scope.
struct Input: InputType {
let increment = PassthroughSubject<Void, Never>()
let isOn = PassthroughSubject<Bool, Never>()
}
Properties of Input are defined internal scope. But these return SubjectProxy
via dynamicMemberLookup if Input is wrapped with InputProxy.
let input: InputProxy<Input>
let increment: SubjectProxy<Void> = input.increment
increment.send()
let isOn: SubjectProxy<Bool> = input.isOn
isOn.send(true)
The rule of Output is having Publisher or @Published
properties that are defined internal scope.
class Output: OutputType {
let count: AnyPublisher<String?, Never>
@Published var isIncrementEnabled: Bool
}
The rule of Store is having inner states.
class Store: StoreType {
@Published var count = 0
@Published var isIncrementEnabled: Bool = false
}
The rule of Extra is having other dependencies.
The rule of Resolver is generating Output from Input, Store and Extra. It generates Output to call static func polish(input:store:extra:)
. static func polish(input:store:extra:)
is called once when Machine is initialized.
enum Resolver: ResolverType {
typealias Input = ViewModel.Input
typealias Output = ViewModel.Output
typealias Store = ViewModel.Store
typealias Extra = ViewModel.Extra
static func polish(input: Publishing<Input>, store: Store, extra: Extra) -> Polished<Output> {
...
}
}
Here is a exmaple of implementation of static func polish(input:store:extra:)
.
extension Resolver {
static func polish(input: Publishing<Input>,
store: Store,
extra: Extra) -> Polished<Output> {
var cancellables: [AnyCancellable] = []
let increment = input.increment
.flatMap { _ in Just(store.count) }
.map { $0 + 1 }
increment.merge(with: decrement)
.assign(to: \.count, on: store)
.store(in: &cancellables)
let count = store.$count
.map(String.init)
.map(Optional.some)
.eraseToAnyPublisher()
return Polished(output: Output(count: count),
cancellables: cancellables)
}
}
Machine represents ViewModels of MVVM (it can also be used as Models). It has input: InputProxy<Input>
and output: OutputProxy<Output>
. It automatically generates input: InputProxy<Input>
and output: OutputProxy<Output>
from instances of Input, Store, Extra and Resolver.
final class ViewModel: Machine<ViewModel> {
final class Input: InputType {
let increment = PassthroughSubject<Void, Never>()
let decrement = PassthroughSubject<Void, Never>()
}
final class Store: StoredOutputType {
@Published var count: Int = 0
}
final class Output: OutputType {
let count: AnyPublisher<String?, Never>
}
struct Extra: ExtraType {}
static func polish(
input: Publishing<Input>,
store: Store,
extra: Extra
) -> Polished<Store> {
var cancellables: [AnyCancellable] = []
let increment = input.increment
.flatMap { _ in Just(store.count) }
.map { $0 + 1 }
let decrement = input.decrement
.flatMap { _ in Just(store.count) }
.map { $0 - 1 }
increment.merge(with: decrement)
.assign(to: \.count, on: store)
.store(in: &cancellables)
let count = store.$count
.map(String.init)
.map(Optional.some)
.eraseToAnyPublisher()
return Polished(output: Output(count: count),
cancellables: cancellables)
}
}
If Input implements BindableInputType
, can access value as Binding<Value>
from outside.
In addition, if Output equals Store and implements StoredOutputType
, can access primitive value and Publisher from outside.
Sample implementaion is here.
final class ViewModel: Machine<ViewModel> {
typealias Output = Store
final class Input: BindableInputType {
let increment = PassthroughSubject<Void, Never>()
let decrement = PassthroughSubject<Void, Never>()
}
final class Store: StoredOutputType {
@Published var count: Int = 0
}
struct Extra: ExtraType {}
static func polish(
input: Publishing<Input>,
store: Store,
extra: Extra
) -> Polished<Store> {
var cancellables: [AnyCancellable] = []
let increment = input.increment
.flatMap { _ in Just(store.count) }
.map { $0 + 1 }
let decrement = input.decrement
.flatMap { _ in Just(store.count) }
.map { $0 - 1 }
increment.merge(with: decrement)
.assign(to: \.count, on: store)
.store(in: &cancellables)
return Polished(cancellables: cancellables)
}
}
let viewModel: ViewModel = ...
viewModel.input.isOn // This is `Binding<Bool>` instance.
viewModel.output.count // This is `Int` instance.
viewModel.output.$count // This is `Published<Int>.Publisher` instance.
- Xcode 12
- macOS 10.15
- iOS 13.0
- tvOS 13.0
- watchOS 6.0
- cats-oss/Unio
- A sister library of Ricemill that runs on RxSwift
- GitHubSearchWithSwiftUI
- An example of GitHub Repository Search App with Ricemill
Ricemill is released under the MIT License.