• Stars
    star
    159
  • Rank 235,916 (Top 5 %)
  • Language
    Swift
  • License
    MIT License
  • Created almost 6 years ago
  • Updated almost 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

πŸ”„ KeyPath based Unidirectional Input / Output framework with RxSwift.

Unidirectional Input Output framework

Build Status License Platform
Carthage compatible Version Carthage compatible

Introduction

Ordinary ViewModels of MVVM might be implemented like this. There are two inputs which one is a input from outside (func search(query:)), another is a input relay for inside (_search: PublishRelay). These inputs can be together as one if it is possible to express something that can only be received inside and can only input outside.

In addition, there are two outputs which one is a observable property ( repositories: Observable<[Repository]>), another is a computed property (repositoriesValue: [Repository]). These outputs are related an inner state (_repositories: BehaviorRelay<[Repository]>). These outputs can be together as one if it is possible to express something that can only be received outside and can only input inside.

class SearchViewModel {
    let repositories: Observable<[Repository]>
    let error: Observable<Error>

    var repositoriesValue: [Repository] {
        return _repositories.value
    }

    private let _repositories = BehaviorRelay<[Repository]>(value: [])
    private let _search = PublishRelay<String>()
    private let disposeBag = DisposeBag()

    init() {
        let apiAciton = SearchAPIAction()

        self.repositories = _repositories.asObservable()
        self.error = apiAction.error

        apiAction.response
            .bind(to: _repositories)
            .disposed(by: disposeBag)

        _search
            .subscribe(onNext: { apiAction.execute($0) })
            .disposed(by: disposeBag)
    }

    func search(query: String) {
        _search.accept(query)
    }
}

About Unio

Unio is KeyPath based Unidirectional Input / Output framework that works with RxSwift. It resolves above issues by using those components.

Input

The rule of Input is having PublishRelay (or PublishSubject) properties that are defined internal scope.

struct Input: InputType {
    let searchText = PublishRelay<String?>()
    let buttonTap = PublishSubject<Void>()
}

Properties of Input are defined internal scope. But these can only access func accept(_:) (or AnyObserver) via KeyPath if Input is wrapped with InputWrapper.

let input: InputWrapper<Input>

input.searchText("query")  // accesses `func accept(_:)`
input.buttonTap.onNext(()) // accesses `AnyObserver`

Output

The rule of Output is having BehaviorRelay (or BehaviorSubject and so on) properties that are defined internal scope.

struct Output: OutputType {
    let repositories: BehaviorRelay<[GitHub.Repository]>
    let isEnabled: BehaviorSubject<Bool>
    let error: Observable<Error>
}

Properties of Output are defined internal scope. But these can only access func asObservable() via KeyPath if Output is wrapped with OutputWrapper.

let output: OutputWrapper<Output>

output.repositories
    .subscribe(onNext: { print($0) })

output.isEnabled
    .subscribe(onNext: { print($0) })

output.error
    .subscribe(onNext: { print($0) })

If a property is BehaviorRelay (or BehaviorSubject), be able to access value via KeyPath.

let p: Property<[GitHub.Repository]> = output.repositories
p.value

let t: ThrowableProperty<Bool> = output.isEnabled
try? t.throwableValue()

If a property is defined as Computed, be able to access computed value.

struct Output: OutputType {
    let isEnabled: Computed<Bool>
}

var _isEnabled = false
let output = OutputWrapper(.init(isEnabled: Computed<Bool> { _isEnabled }))

output.isEnabled // false
_isEnabled = true
output.isEnabled // true

State

The rule of State is having inner states of UnioStream.

struct State: StateType {
    let repositories = BehaviorRelay<[GitHub.Repository]>(value: [])
}

Extra

The rule of Extra is having other dependencies of UnioStream.

struct Extra: ExtraType {
    let apiStream: GitHubSearchAPIStream
}

Logic

The rule of Logic is generating Output from Dependency<Input, State, Extra>. It generates Output to call static func bind(from:disposeBag:). static func bind(from:disposeBag:) is called once when UnioStream is initialized.

enum Logic: LogicType {
    typealias Input = GitHubSearchViewStream.Input
    typealias Output = GitHubSearchViewStream.Output
    typealias State = GitHubSearchViewStream.State
    typealias Extra = GitHubSearchViewStream.Extra

    static func bind(from dependency: Dependency<Input, State, Extra>, disposeBag: DisposeBag) -> Output
}

Connect sequences and generate Output in static func bind(from:disposeBag:) to use below properties and methods.

  • dependency.state
  • dependency.extra
  • dependency.inputObservables ... Returns a Observable that is property of Input.
  • disposeBag ... Same lifecycle with UnioStream.

Here is a exmaple of implementation.

extension Logic {

    static func bind(from dependency: Dependency<Input, State, Extra>, disposeBag: DisposeBag) -> Output {
        let apiStream = dependency.extra.apiStream

        dependency.inputObservables.searchText
            .bind(to: apiStream.searchText)
            .disposed(by: disposeBag)

        let repositories = apiStream.output.searchResponse
            .map { $0.items }

        return Output(repositories: repositories)
    }
}

UnioStream

UnioStream represents ViewModels of MVVM (it can also be used as Models). It has input: InputWrapper<Input> and output: OutputWrapper<Output>. It automatically generates input: InputWrapper<Input> and output: OutputWrapper<Output> from instances of Input, State, Extra and Logic.

typealias UnioStream<Logic: LogicType> = PrimitiveStream<Logic> & LogicType

class PrimitiveStream<Logic: LogicType> {

    let input: InputWrapper<Logic.Input>
    let output: OutputWrapper<Logic.Output>

    init(input: Logic.Input, state: Logic.State, extra: Logic.Extra)
}

Be able to define a subclass of UnioStream like this.

final class GitHubSearchViewStream: UnioStream<GitHubSearchViewStream> {

    convenience init() {
        self.init(input: Input(), state: State(), extra: Extra())
    }
}

Usage

Here is an example.

Define GitHubSearchViewStream for searching GitHub repositories.

protocol GitHubSearchViewStreamType: AnyObject {
    var input: InputWrapper<GitHubSearchViewStream.Input> { get }
    var output: OutputWrapper<GitHubSearchViewStream.Output> { get }
}

final class GitHubSearchViewStream: UnioStream<GitHubSearchViewStream>, GitHubSearchViewStreamType {

    convenience init() {
        self.init(input: Input(), state: State(), extra: Extra())
    }

    typealias State = NoState

    struct Input: InputType {
        let searchText = PublishRelay<String?>()
    }

    struct Output: OutputType {
        let repositories: Observable<[GitHub.Repository]>
    }

    struct Extra: ExtraType {
        let apiStream: GitHubSearchAPIStream()
    }

    static func bind(from dependency: Dependency<Input, State, Extra>, disposeBag: DisposeBag) -> Output {
        let apiStream = dependency.extra.apiStream

        dependency.inputObservables.searchText
            .bind(to: apiStream.input.searchText)
            .disposed(by: disposeBag)

        let repositories = apiStream.output.searchResponse
            .map { $0.items }

        return Output(repositories: repositories)
    }
}

Bind searchBar text to viewStream input. On the other hand, bind viewStream output to tableView data source.

final class GitHubSearchViewController: UIViewController {

    let searchBar = UISearchBar(frame: .zero)
    let tableView = UITableView(frame: .zero)

    private let viewStream: GitHubSearchViewStreamType = GitHubSearchViewStream()
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

        searchBar.rx.text
            .bind(to: viewStream.input.searchText)
            .disposed(by: disposeBag)

        viewStream.output.repositories
            .bind(to: tableView.rx.items(cellIdentifier: "Cell")) {
                (row, repository, cell) in
                cell.textLabel?.text = repository.fullName
                cell.detailTextLabel?.text = repository.htmlUrl.absoluteString
            }
            .disposed(by: disposeBag)
    }
}

The documentation which does not use KeyPath Dynamic Member Lookup is here.

Migration Guides

Xcode Template

You can use Xcode Templates for Unio. Let's install with ./Tools/install-xcode-template.sh command!

Installation

Carthage

If you’re using Carthage, simply add Unio to your Cartfile:

github "cats-oss/Unio"

CocoaPods

Unio is available through CocoaPods. To install it, simply add the following line to your Podfile:

pod "Unio"

Swift Package Manager

Simply add the following line to your Package.swift:

.package(url: "https://github.com/cats-oss/Unio.git", from: "version")

Requirements

  • Swift 5 or greater
  • iOS 9.0 or greater
  • tvOS 10.0 or greater
  • watchOS 3.0 or greater
  • macOS 10.10 or greater
  • RxSwift 6.0 or greater

License

Unio is released under the MIT License.

More Repositories

1

android-gpuimage

Android filters based on OpenGL (idea from GPUImage for iOS)
Java
8,952
star
2

Sica

🦌 Simple Interface Core Animation. Run type-safe animation sequencially or parallelly
Swift
1,056
star
3

VideoCast-Swift

A framework for broadcasting live video
Swift
190
star
4

Chausie

Chausie provides a customizable view containers that manages navigation between pages of content. 🐱
Swift
178
star
5

use-intersection

React Hooks for IntersectionObserver.
TypeScript
108
star
6

android-tab-animation

Easily create TabLayout.Tab animations that sync with the scrolling progress of ViewPager
Kotlin
104
star
7

android-license-sample

Kotlin
86
star
8

AcknowledgementsPlist

AcknowledgementsPlist manages the licenses of libraries that depend on your iOS app.
Swift
76
star
9

Matcha

🍡 Matcha is Framework that can get parameters from URL Path.
Swift
58
star
10

grpc-swift-client

πŸ” Client-side library that depends on SwiftGRPC which is a library of gRPC written in Swift.
Swift
49
star
11

cujira

🐳 cujira is a command line tool that makes easy to show issue list from Jira.
Swift
48
star
12

fastlane-plugin-firebase_test_lab_android

Test your app with Firebase Test Lab with ease using fastlane for Android
Ruby
48
star
13

RxDucks

πŸ¦† RxDucks is a Redux-like framework working on RxSwift.
Swift
40
star
14

Ship

🚒 Ship is a APIKit plugin that can inject common processing to requests on APIKit.
Swift
28
star
15

yarn-outdated-notifier

πŸš€ Add link to CHANGELOG the result of $ yarn outdated, and notify to GitHub Issue.
JavaScript
25
star
16

Degu

🐭 Degu is debug utility for iOS, tvOS and macOS.
Swift
24
star
17

Mag

🧲Mag is Framework which wraps NSLayoutAnchor.
Swift
21
star
18

kite

A Kotlin DSL to bind Android UI components to your app state.
Kotlin
20
star
19

github-action-auto-assign

The GitHub Actions assigns reviewers & changes the status labels.
JavaScript
18
star
20

xcbt

βŒ›οΈxcbt shows Xcode build time of a specified project.
Swift
14
star
21

intly

intly is Type-safe (TS friendly) i18n library.
TypeScript
12
star
22

ts-proto-optimize

It's CLI tool for optimize TypeScript protobuf type-definition file.
TypeScript
9
star
23

fastlane-plugin-accessibility_test

Java
8
star
24

ExtensionProperty

πŸ—œThe utility interface for Associated Object
Swift
8
star
25

fastlane-plugin-snapshot_test

Ruby
7
star
26

docker-node-headless-chrome-ja

🐳 Docker image for headless Chrome.
Dockerfile
6
star
27

github-action-detect-unmergeable

The GitHub Action to detect an unmergeable pull request on changing its upstream.
Go
5
star
28

eslint-config-abema

This project is presets of eslint configurations which we used in our some internal projects.
JavaScript
4
star
29

docker-node-headless-chrome

Docker Image for Chrome headless.
Dockerfile
3
star
30

fastlane-plugin-screenshot_notifier

2
star
31

hugo-cats-studio

A simple and minimal Hugo theme.
CSS
1
star