Unio

by cats-oss

cats-oss / Unio

🔄 KeyPath based Unidirectional Input / Output framework with RxSwift.

133 Stars 6 Forks Last release: 6 months ago (0.10.0) MIT License 98 Commits 11 Releases

Available items

No Items, yet!

The developer of this repository has not created any items for sale yet. Need a bug fixed? Help with integration? A different license? Create a request here:

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
), another is a computed property (
repositoriesValue: [Repository]
). These outputs are related an inner state (
_repositories: BehaviorRelay
). 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
    let error: Observable

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

private let _repositories = BehaviorRelay(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()
    let buttonTap = PublishSubject()
}

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.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
    let isEnabled: BehaviorSubject
    let error: Observable
}

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.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 = output.repositories
p.value

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

If a property is defined as

Computed
, be able to access computed value.
struct Output: OutputType {
    let isEnabled: Computed
}

var _isEnabled = false let output = OutputWrapper(.init(isEnabled: Computed { _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(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. 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) -&gt; 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) -&gt; 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
and
output: OutputWrapper
. It automatically generates
input: InputWrapper
and
output: OutputWrapper
from instances of Input, State, Extra and Logic.
typealias UnioStream = PrimitiveStream & 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)

} </logic.output></logic.input>

Be able to define a subclass of UnioStream like this.

final class GitHubSearchViewStream: UnioStream {

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 { get }
    var output: OutputWrapper { get }
}

final class GitHubSearchViewStream: UnioStream, 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
}

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

static func bind(from dependency: Dependency<input state extra>, disposeBag: DisposeBag) -&gt; 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)
}

} </githubsearchviewstream.output></githubsearchviewstream.input>

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 5.0 or greater

License

Unio is released under the MIT License.

We use cookies. If you continue to browse the site, you agree to the use of cookies. For more information on our use of cookies please see our Privacy Policy.