Ricemill

Unidirectional Input / Output framework with Combine. Supports both of SwiftUI and UIKit.

Ricemill represents unidirectional data flow with these components.

SwiftUI-Playground

UIKit-Playground

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

Ricemill

GitHub