Ricemill(米磨)

🌾 ♻️ 🍚 使用 Combine 的单向输入/输出框架。

SwiftUI Playground UIKit Playground
SwiftUI UIKit

关于 Ricemill

Ricemill 使用以下组件来表示单向数据流。

输入 (Input)

输入的规则是拥有在内部作用域定义的 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)

输出 (Output)

输出的规则是拥有在内部作用域定义的 Publisher 或 @Published 属性。

class Output: OutputType {
    let count: AnyPublisher<String?, Never>
    @Published var isIncrementEnabled: Bool
}

存储 (Store)

存储的规则是拥有内部状态。

class Store: StoreType {
    @Published var count = 0
    @Published var isIncrementEnabled: Bool = false
}

额外 (Extra)

额外的规则是拥有其他依赖项。

解析器 (Resolver)

解析器的规则是从输入、存储和额外生成输出。它通过调用 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)

Machine 代表 MVVM 的 ViewModels(它也可以用作 Models)。它拥有 input: InputProxy<Input>output: OutputProxy<Output>。 它自动从 InputStoreExtraResolver 的实例生成 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)
    }
}

SwiftUI 用法

如果 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.

要求

其他链接

screenshot

许可证

Ricemill 根据 MIT 许可证发布。