Unidirectional Input / Output framework with Combine
Ricemill
Unidirectional Input / Output framework with Combine. Supports both of SwiftUI and UIKit.
Ricemill represents unidirectional data flow with these components.
Input
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)
Output
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
}
Store
The rule of Store is having inner states.
class Store: StoreType {
@Published var count = 0
@Published var isIncrementEnabled: Bool = false
}
Extra
The rule of Extra is having other dependencies.
Resolver
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 ViewModel.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
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.
SwiftUI Usage
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.
extension ViewModel {
typealias Output = Store
final class Input: BindableInputType {
let increment = PassthroughSubject<Void, Never>()
@Published var isOn = false
}
final class Store: StoredOutputType {
@Published var count: Int = 0
}
}
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.
Requirement
- Xcode 11 Beta 5
- macOS 10.15
- iOS 13.0
- tvOS 13.0
- watchOS 6.0
Other links
- cats-oss/Unio
- A sister library of Ricemill that runs on RxSwift
- GitHubSearchWithSwiftUI
- An example of GitHub Repository Search App with Ricemill