🌾 ♻️ 🍚 使用 Combine 的单向输入/输出框架。
SwiftUI Playground | UIKit Playground |
---|---|
![]() |
![]() |
Ricemill 使用以下组件来表示单向数据流。
输入的规则是拥有在内部作用域定义的 Subject 属性。
struct Input: InputType {
let increment = PassthroughSubject<Void, Never>()
let isOn = PassthroughSubject<Bool, Never>()
}
Input 的属性在内部作用域定义。但是,如果 Input 被 InputProxy 包装,这些属性会通过 dynamicMemberLookup 返回 SubjectProxy
。
let input: InputProxy<Input>
let increment: SubjectProxy<Void> = input.increment
increment.send()
let isOn: SubjectProxy<Bool> = input.isOn
isOn.send(true)
输出的规则是拥有在内部作用域定义的 Publisher 或 @Published
属性。
class Output: OutputType {
let count: AnyPublisher<String?, Never>
@Published var isIncrementEnabled: Bool
}
存储的规则是拥有内部状态。
class Store: StoreType {
@Published var count = 0
@Published var isIncrementEnabled: Bool = false
}
额外的规则是拥有其他依赖项。
解析器的规则是从输入、存储和额外生成输出。它通过调用 static func polish(input:store:extra:)
来生成输出。static func polish(input:store:extra:)
在 Machine 初始化时调用一次。
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> {
...
}
}
这是一个 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 代表 MVVM 的 ViewModels(它也可以用作 Models)。它拥有 input: InputProxy<Input>
和 output: OutputProxy<Output>
。 它自动从 Input、Store、Extra 和 Resolver 的实例生成 input: InputProxy<Input>
和 output: OutputProxy<Output>
。
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)
}
}
如果 Input 实现了 BindableInputType
,则可以从外部以 Binding<Value>
的形式访问值。 此外,如果 Output 等于 Store 并且实现了 StoredOutputType
,则可以从外部访问原始值和 Publisher。 此处是示例实现。
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.
Ricemill 根据 MIT 许可证发布。