Ricemill
🌾 ♻️ 🍚 Unidirectional Input / Output framework with Combine.
About Ricemill
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)
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
}
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 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.
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)
}
}
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.
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.
Requirement
- Xcode 12
- macOS 10.15
- iOS 13.0
- tvOS 13.0
- watchOS 6.0
Other links

License
Ricemill is released under the MIT License.
Ricemill
🌾 ♻️ 🍚 Unidirectional Input / Output framework with Combine.
About Ricemill
Ricemill represents unidirectional data flow with these components.
Input
The rule of Input is having Subject properties that are defined internal scope.
Properties of Input are defined internal scope. But these return
SubjectProxy
via dynamicMemberLookup if Input is wrapped with InputProxy.Output
The rule of Output is having Publisher or
@Published
properties that are defined internal scope.Store
The rule of Store is having inner states.
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.Here is a exmaple of implementation of
static func polish(input:store:extra:)
.Machine
Machine represents ViewModels of MVVM (it can also be used as Models). It has
input: InputProxy<Input>
andoutput: OutputProxy<Output>
. It automatically generatesinput: InputProxy<Input>
andoutput: OutputProxy<Output>
from instances of Input, Store, Extra and Resolver.SwiftUI Usage
If Input implements
BindableInputType
, can access value asBinding<Value>
from outside. In addition, if Output equals Store and implementsStoredOutputType
, can access primitive value and Publisher from outside. Sample implementaion is here.Requirement
Other links
License
Ricemill is released under the MIT License.