Skip to content
πŸ”„ KeyPath based Unidirectional Input / Output framework with RxSwift.
Branch: master
Clone or download
Latest commit f837a9d May 7, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
Example update xcodeproj May 7, 2019
Tools update template Apr 3, 2019
Unio.xcodeproj fix UnioTests May 7, 2019
Unio update RxSwift to v5 May 7, 2019
UnioTests fix UnioTests May 7, 2019
.gitignore add project Mar 21, 2019
.travis.yml
Cartfile update RxSwift to v5 May 7, 2019
Cartfile.resolved update RxSwift to v5 May 7, 2019
LICENSE
README.md Merge remote-tracking branch 'origin/master' into support-rxswift5 May 7, 2019
Unio.podspec update podspec May 7, 2019

README.md

Unidirectional Input Output framework

Carthage compatible Build Status Version License Platform

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 func on(_:)) via KeyPath if Input is wrapped with Relay.

let input: Relay<Input>

input.accept("query", for: \.searchText)
input.onEvent(.next(()), for: \.buttonTap)

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 Relay.

let output: Relay<Output>

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

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

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

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

output.value(for: \.repositories)

try? output.value(for: \.isEnabled)

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 func bind(from:). func bind(from:) is called once when UnioStream is initialized. If you want to use DisposeBags in func bind(from:), define properties of DisposeBag in Logic.

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

    let disposeBag = DisposeBag()

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

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

  • dependency.state
  • dependency.extra
  • dependency.inputObservable(for:) ... returns a Observable that is property of Input.
  • dependency.readOnlyReference(from:for:) ... returns a read only BehaviorRelay (or BehaviorSubject) (that is wrapped by ReadOnly<T>) from property of Output.

Here is a exmaple of implementation.

extension GitHubSearchViewStream.Logic {

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

        dependency.inputObservable(for: \.searchText)
            .bind(to: apiStream.input.accept(for: \.searchText))
            .disposed(by: disposeBag)

        let repositories = apiStream.output
            .observable(for: \.searchResponse)
            .map { $0.items }

        return Output(repositories: repositories)
    }
}

UnioStream

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

class UnioStream<Logic: LogicType> {

    let input: Relay<Logic.Input>
    let output: Relay<Logic.Output>

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

Be able to define a subclass of UnioStream like this.

fianl class GitHubSearchViewStream: UnioStream<GitHubSearchViewStream.Logic> {

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

Usage

Here is an example.

Define GitHubSearchViewStream for searching GitHub repositories.

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

final class GitHubSearchViewStream: UnioStream<GitHubSearchViewStream.Logic>, GitHubSearchViewStreamType {

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

    typealias State = NoState

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

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

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

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

        let disposeBag = DisposeBag()
    }
}

extension GitHubSearchViewStream.Logic {

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

        dependency.inputObservable(for: \.searchText)
            .bind(to: apiStream.input.accept(for: \.searchText))
            .disposed(by: disposeBag)

        let repositories = apiStream.output
            .observable(for: \.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 = GitHubSearchViewStream()
    private let disposeBag = DisposeBag()

    override func viewDidLoad() {
        super.viewDidLoad()

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

        viewStream.output.observable(for: \.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)
    }
}

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"

Requirements

  • Swift 5 or greater
  • iOS 9.0 or greater
  • RxSwift 5.0 or greater

License

Unio is released under the MIT License.

You can’t perform that action at this time.