Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/marty-suzuki/ricemill

🌾 ♻️ 🍚 Unidirectional Input / Output framework with Combine. Supports both of SwiftUI and UIKit.
https://github.com/marty-suzuki/ricemill

combine mvvm swift swiftui

Last synced: 2 months ago
JSON representation

🌾 ♻️ 🍚 Unidirectional Input / Output framework with Combine. Supports both of SwiftUI and UIKit.

Awesome Lists containing this project

README

        

# Ricemill

🌾 ♻️ 🍚 Unidirectional Input / Output framework with Combine.

| [SwiftUI Playground](https://github.com/marty-suzuki/Ricemill/blob/master/Ricemill.playground/Pages/SwiftUI.xcplaygroundpage/Contents.swift) | [UIKit Playground](https://github.com/marty-suzuki/Ricemill/blob/master/Ricemill.playground/Pages/UIKit.xcplaygroundpage/Contents.swift) |
| :-: | :-: |
| ![SwiftUI](https://user-images.githubusercontent.com/2082134/63072558-68a5b780-bf5f-11e9-81e8-d25798ec29da.gif) | ![UIKit](https://user-images.githubusercontent.com/2082134/63072557-67748a80-bf5f-11e9-9f9f-fe6510421340.gif) |

# About Ricemill

Ricemill represents unidirectional data flow with these components.

- [Input](#input)
- [Output](#output)
- [Store](#store)
- [Extra](#extra)
- [Resolver](#resolver)
- [Machine](#machine)

### Input

The rule of Input is having Subject properties that are defined internal scope.

```swift
struct Input: InputType {
let increment = PassthroughSubject()
let isOn = PassthroughSubject()
}
```

Properties of Input are defined internal scope. But these return `SubjectProxy` via dynamicMemberLookup if Input is wrapped with InputProxy.

```swift
let input: InputProxy
let increment: SubjectProxy = input.increment
increment.send()
let isOn: SubjectProxy = input.isOn
isOn.send(true)
```

### Output

The rule of Output is having Publisher or `@Published` properties that are defined internal scope.

```swift
class Output: OutputType {
let count: AnyPublisher
@Published var isIncrementEnabled: Bool
}
```

### Store

The rule of Store is having inner states.

```swift
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.

```swift
enum Resolver: ResolverType {
typealias Input = ViewModel.Input
typealias Output = ViewModel.Output
typealias Store = ViewModel.Store
typealias Extra = ViewModel.Extra

static func polish(input: Publishing, store: Store, extra: Extra) -> Polished {
...
}
}
```

Here is a exmaple of implementation of `static func polish(input:store:extra:)`.

```swift
extension Resolver {

static func polish(input: Publishing,
store: Store,
extra: Extra) -> Polished {

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` and `output: OutputProxy`. It automatically generates `input: InputProxy` and `output: OutputProxy` from instances of [Input](#input), [Store](#store), [Extra](#extra) and [Resolver](#resolver).

```swift
final class ViewModel: Machine {

final class Input: InputType {
let increment = PassthroughSubject()
let decrement = PassthroughSubject()
}

final class Store: StoredOutputType {
@Published var count: Int = 0
}

final class Output: OutputType {
let count: AnyPublisher
}

struct Extra: ExtraType {}

static func polish(
input: Publishing,
store: Store,
extra: Extra
) -> Polished {
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` from outside.
In addition, if Output equals Store and implements `StoredOutputType`, can access primitive value and Publisher from outside.
Sample implementaion is here.

```swift
final class ViewModel: Machine {
typealias Output = Store

final class Input: BindableInputType {
let increment = PassthroughSubject()
let decrement = PassthroughSubject()
}

final class Store: StoredOutputType {
@Published var count: Int = 0
}

struct Extra: ExtraType {}

static func polish(
input: Publishing,
store: Store,
extra: Extra
) -> Polished {
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` instance.
viewModel.output.count // This is `Int` instance.
viewModel.output.$count // This is `Published.Publisher` instance.
```

# Requirement

- Xcode 12
- macOS 10.15
- iOS 13.0
- tvOS 13.0
- watchOS 6.0

# Other links

- [cats-oss/Unio](https://github.com/cats-oss/Unio)
- A sister library of Ricemill that runs on RxSwift
- [GitHubSearchWithSwiftUI](https://github.com/marty-suzuki/GitHubSearchWithSwiftUI/blob/ricemill-sample/GitHubSearchWithSwiftUI/View/RepositoryListViewModel.swift)
- An example of GitHub Repository Search App with Ricemill

![screenshot](https://user-images.githubusercontent.com/2082134/63103899-ef3ab300-bfb8-11e9-89d4-2c7f5f1a73da.png)

# License

Ricemill is released under the MIT License.