Coder Social home page Coder Social logo

cats-oss / unio Goto Github PK

View Code? Open in Web Editor NEW
159.0 4.0 7.0 3.74 MB

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

License: MIT License

Swift 99.18% Shell 0.37% Objective-C 0.13% Ruby 0.32%
mvvm rxswift unidirectional keypath

unio's Introduction

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.

unio's People

Contributors

akkyie avatar funzin avatar marty-suzuki avatar tomosaaan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

unio's Issues

Reduce README.md Resources

Xcode Template GIF image

file_gen

Input to Relay Example

input

Output to Relay Example

output

GitHub Repository Search Example GIF image

search

Xcode Templates

template

Unio Logo with text

unio_logo

Unio Logo

unio

Support Key Path Member Lookup

SE-0252 (Key Path Member Lookup) has been implemented on Swift 5.1, and is going to be available with the next release of Xcode.

As described in the SE, it allows you to access a property of a type, using different type's KeyPath. You can return the "original" property with the KeyPath, after optionally transforming it:

// Taken from the SE
extension Lens {
  func project<U>(_ keyPath: WritableKeyPath<T, U>) -> Lens<U> {
    return Lens<U>(
        getter: { self.value[keyPath: keyPath] },
        setter: { self.value[keyPath: keyPath] = $0 })
  }
}

It may greatly enhance Unio's ergonomics. You will be able to write your binding inside and outside of a UnioStream, simply and type-safe (with auto-completion(!)) while keeping its unidirectional flow:

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

searchBar.rx.text.onNext // -> compile error

I hope I can make a PR with actual implementation before Swift 5.1 get released.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.