{"id":19943144,"url":"https://github.com/cats-oss/unio","last_synced_at":"2025-08-20T06:32:02.399Z","repository":{"id":34361460,"uuid":"176978860","full_name":"cats-oss/Unio","owner":"cats-oss","description":"🔄 KeyPath based Unidirectional Input / Output framework with RxSwift.","archived":false,"fork":false,"pushed_at":"2022-02-10T09:01:37.000Z","size":3829,"stargazers_count":159,"open_issues_count":0,"forks_count":7,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-12-13T04:38:40.630Z","etag":null,"topics":["keypath","mvvm","rxswift","unidirectional"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cats-oss.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2019-03-21T15:57:01.000Z","updated_at":"2023-04-27T15:48:06.000Z","dependencies_parsed_at":"2022-08-08T00:16:37.964Z","dependency_job_id":null,"html_url":"https://github.com/cats-oss/Unio","commit_stats":null,"previous_names":[],"tags_count":12,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cats-oss%2FUnio","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cats-oss%2FUnio/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cats-oss%2FUnio/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cats-oss%2FUnio/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cats-oss","download_url":"https://codeload.github.com/cats-oss/Unio/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":230400606,"owners_count":18219830,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["keypath","mvvm","rxswift","unidirectional"],"created_at":"2024-11-13T00:15:30.613Z","updated_at":"2024-12-19T08:07:44.364Z","avatar_url":"https://github.com/cats-oss.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cp align='center'\u003e\n\u003cimg src='https://user-images.githubusercontent.com/2082134/54809507-d9a1b580-4cc6-11e9-93cf-2ffd8e1e952c.png' width='200px'\u003e\n\u003c/p\u003e\n\u003cp align='center'\u003e\n\u003cstrong\u003eUn\u003c/strong\u003eidirectional \u003cstrong\u003eI\u003c/strong\u003enput \u003cstrong\u003eO\u003c/strong\u003eutput framework\n\u003c/p\u003e\n\u003cp align='center'\u003e\n  \u003ca href='https://travis-ci.org/cats-oss/Unio'\u003e\n    \u003cimg src='https://travis-ci.org/cats-oss/Unio.svg?branch=master' alt='Build Status' /\u003e\n  \u003c/a\u003e\n\n  \u003ca href='https://cocoapods.org/pods/Unio'\u003e\n    \u003cimg src='https://img.shields.io/cocoapods/l/Unio.svg?style=flat' alt='License' /\u003e\n  \u003c/a\u003e\n  \u003ca href='https://cocoapods.org/pods/Unio'\u003e\n    \u003cimg src='https://img.shields.io/cocoapods/p/Unio.svg?style=flat' alt='Platform' /\u003e\n  \u003c/a\u003e\n  \u003cbr/\u003e\n  \u003ca href='https://github.com/Carthage/Carthage'\u003e\n    \u003cimg src='https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat' alt='Carthage compatible' /\u003e\n  \u003c/a\u003e\n  \u003ca href='https://cocoapods.org/pods/Unio'\u003e\n    \u003cimg src='https://img.shields.io/cocoapods/v/Unio.svg?style=flat' alt='Version' /\u003e\n  \u003c/a\u003e\n  \u003ca href='https://swift.org/package-manager/'\u003e\n    \u003cimg src='https://img.shields.io/badge/Swift%20Package%20Manager-compatible-4BC51D.svg?style=flat' alt='Carthage compatible' /\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n## Introduction\n\nOrdinary 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*.\n\nIn addition, there are two outputs which one is a observable property ( `repositories: Observable\u003c[Repository]\u003e`), another is a computed property (`repositoriesValue: [Repository]`). These outputs are related an inner state (`_repositories: BehaviorRelay\u003c[Repository]\u003e`). 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*.\n\n```swift\nclass SearchViewModel {\n    let repositories: Observable\u003c[Repository]\u003e\n    let error: Observable\u003cError\u003e\n\n    var repositoriesValue: [Repository] {\n        return _repositories.value\n    }\n\n    private let _repositories = BehaviorRelay\u003c[Repository]\u003e(value: [])\n    private let _search = PublishRelay\u003cString\u003e()\n    private let disposeBag = DisposeBag()\n\n    init() {\n        let apiAciton = SearchAPIAction()\n\n        self.repositories = _repositories.asObservable()\n        self.error = apiAction.error\n\n        apiAction.response\n            .bind(to: _repositories)\n            .disposed(by: disposeBag)\n\n        _search\n            .subscribe(onNext: { apiAction.execute($0) })\n            .disposed(by: disposeBag)\n    }\n\n    func search(query: String) {\n        _search.accept(query)\n    }\n}\n```\n\n## About Unio\n\nUnio is KeyPath based **Un**idirectional **I**nput / **O**utput framework that works with RxSwift.\nIt resolves [above issues](#introduction) by using those components.\n\n- [Input](#input)\n- [Output](#output)\n- [State](#state)\n- [Extra](#extra)\n- [Logic](#logic)\n- [UnioStream](#uniostream)\n\n### Input\n\nThe rule of Input is having PublishRelay (or PublishSubject) properties that are defined internal scope.\n\n```swift\nstruct Input: InputType {\n    let searchText = PublishRelay\u003cString?\u003e()\n    let buttonTap = PublishSubject\u003cVoid\u003e()\n}\n```\n\nProperties of Input are defined internal scope.\nBut these can only access `func accept(_:)` (or `AnyObserver`) via KeyPath if Input is wrapped with `InputWrapper`.\n\n```swift\nlet input: InputWrapper\u003cInput\u003e\n\ninput.searchText(\"query\")  // accesses `func accept(_:)`\ninput.buttonTap.onNext(()) // accesses `AnyObserver`\n```\n\n![](https://user-images.githubusercontent.com/2082134/64858916-afbcc080-d663-11e9-8a70-92a9293f7c83.png)\n\n### Output\n\nThe rule of Output is having BehaviorRelay (or BehaviorSubject and so on) properties that are defined internal scope.\n\n```swift\nstruct Output: OutputType {\n    let repositories: BehaviorRelay\u003c[GitHub.Repository]\u003e\n    let isEnabled: BehaviorSubject\u003cBool\u003e\n    let error: Observable\u003cError\u003e\n}\n```\n\nProperties of Output are defined internal scope.\nBut these can only access `func asObservable()` via KeyPath if Output is wrapped with `OutputWrapper`.\n\n```swift\nlet output: OutputWrapper\u003cOutput\u003e\n\noutput.repositories\n    .subscribe(onNext: { print($0) })\n\noutput.isEnabled\n    .subscribe(onNext: { print($0) })\n\noutput.error\n    .subscribe(onNext: { print($0) })\n```\n\nIf a property is BehaviorRelay (or BehaviorSubject), be able to access value via KeyPath.\n\n```swift\nlet p: Property\u003c[GitHub.Repository]\u003e = output.repositories\np.value\n\nlet t: ThrowableProperty\u003cBool\u003e = output.isEnabled\ntry? t.throwableValue()\n```\n\nIf a property is defined as `Computed`, be able to access computed value.\n\n```swift\nstruct Output: OutputType {\n    let isEnabled: Computed\u003cBool\u003e\n}\n\nvar _isEnabled = false\nlet output = OutputWrapper(.init(isEnabled: Computed\u003cBool\u003e { _isEnabled }))\n\noutput.isEnabled // false\n_isEnabled = true\noutput.isEnabled // true\n```\n\n![](https://user-images.githubusercontent.com/2082134/64858314-f7dae380-d661-11e9-9a79-3ca5c53fd90a.png)\n\n### State\n\nThe rule of State is having inner states of [UnioStream](#uniostream).\n\n```swift\nstruct State: StateType {\n    let repositories = BehaviorRelay\u003c[GitHub.Repository]\u003e(value: [])\n}\n```\n\n### Extra\n\nThe rule of Extra is having other dependencies of [UnioStream](#uniostream).\n\n```swift\nstruct Extra: ExtraType {\n    let apiStream: GitHubSearchAPIStream\n}\n```\n\n### Logic\n\nThe rule of Logic is generating [Output](#output) from Dependency\u003cInput, State, Extra\u003e.\nIt generates [Output](#output) to call `static func bind(from:disposeBag:)`.\n`static func bind(from:disposeBag:)` is called once when [UnioStream](#uniostream) is initialized.\n\n```swift\nenum Logic: LogicType {\n    typealias Input = GitHubSearchViewStream.Input\n    typealias Output = GitHubSearchViewStream.Output\n    typealias State = GitHubSearchViewStream.State\n    typealias Extra = GitHubSearchViewStream.Extra\n\n    static func bind(from dependency: Dependency\u003cInput, State, Extra\u003e, disposeBag: DisposeBag) -\u003e Output\n}\n```\n\nConnect sequences and generate [Output](#output) in `static func bind(from:disposeBag:)` to use below properties and methods.\n\n- `dependency.state`\n- `dependency.extra`\n- `dependency.inputObservables` ... Returns a Observable that is property of [Input](#input).\n- `disposeBag` ... Same lifecycle with UnioStream.\n\nHere is a exmaple of implementation.\n\n```swift\nextension Logic {\n\n    static func bind(from dependency: Dependency\u003cInput, State, Extra\u003e, disposeBag: DisposeBag) -\u003e Output {\n        let apiStream = dependency.extra.apiStream\n\n        dependency.inputObservables.searchText\n            .bind(to: apiStream.searchText)\n            .disposed(by: disposeBag)\n\n        let repositories = apiStream.output.searchResponse\n            .map { $0.items }\n\n        return Output(repositories: repositories)\n    }\n}\n```\n\n### UnioStream\n\nUnioStream represents ViewModels of MVVM (it can also be used as Models).\nIt has `input: InputWrapper\u003cInput\u003e` and `output: OutputWrapper\u003cOutput\u003e`.\nIt automatically generates `input: InputWrapper\u003cInput\u003e` and `output: OutputWrapper\u003cOutput\u003e` from instances of [Input](#input), [State](#state), [Extra](#extra) and [Logic](#logic).\n\n```swift\ntypealias UnioStream\u003cLogic: LogicType\u003e = PrimitiveStream\u003cLogic\u003e \u0026 LogicType\n\nclass PrimitiveStream\u003cLogic: LogicType\u003e {\n\n    let input: InputWrapper\u003cLogic.Input\u003e\n    let output: OutputWrapper\u003cLogic.Output\u003e\n\n    init(input: Logic.Input, state: Logic.State, extra: Logic.Extra)\n}\n```\n\nBe able to define a subclass of UnioStream like this.\n\n```swift\nfinal class GitHubSearchViewStream: UnioStream\u003cGitHubSearchViewStream\u003e {\n\n    convenience init() {\n        self.init(input: Input(), state: State(), extra: Extra())\n    }\n}\n```\n\n## Usage\n\nHere is an example.\n\n![](https://user-images.githubusercontent.com/2082134/54809487-bf67d780-4cc6-11e9-83aa-4fa69060702a.gif)\n\nDefine GitHubSearchViewStream for searching GitHub repositories.\n\n```swift\nprotocol GitHubSearchViewStreamType: AnyObject {\n    var input: InputWrapper\u003cGitHubSearchViewStream.Input\u003e { get }\n    var output: OutputWrapper\u003cGitHubSearchViewStream.Output\u003e { get }\n}\n\nfinal class GitHubSearchViewStream: UnioStream\u003cGitHubSearchViewStream\u003e, GitHubSearchViewStreamType {\n\n    convenience init() {\n        self.init(input: Input(), state: State(), extra: Extra())\n    }\n\n    typealias State = NoState\n\n    struct Input: InputType {\n        let searchText = PublishRelay\u003cString?\u003e()\n    }\n\n    struct Output: OutputType {\n        let repositories: Observable\u003c[GitHub.Repository]\u003e\n    }\n\n    struct Extra: ExtraType {\n        let apiStream: GitHubSearchAPIStream()\n    }\n\n    static func bind(from dependency: Dependency\u003cInput, State, Extra\u003e, disposeBag: DisposeBag) -\u003e Output {\n        let apiStream = dependency.extra.apiStream\n\n        dependency.inputObservables.searchText\n            .bind(to: apiStream.input.searchText)\n            .disposed(by: disposeBag)\n\n        let repositories = apiStream.output.searchResponse\n            .map { $0.items }\n\n        return Output(repositories: repositories)\n    }\n}\n```\n\nBind searchBar text to viewStream input. On the other hand, bind viewStream output to tableView data source.\n\n```swift\nfinal class GitHubSearchViewController: UIViewController {\n\n    let searchBar = UISearchBar(frame: .zero)\n    let tableView = UITableView(frame: .zero)\n\n    private let viewStream: GitHubSearchViewStreamType = GitHubSearchViewStream()\n    private let disposeBag = DisposeBag()\n\n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        searchBar.rx.text\n            .bind(to: viewStream.input.searchText)\n            .disposed(by: disposeBag)\n\n        viewStream.output.repositories\n            .bind(to: tableView.rx.items(cellIdentifier: \"Cell\")) {\n                (row, repository, cell) in\n                cell.textLabel?.text = repository.fullName\n                cell.detailTextLabel?.text = repository.htmlUrl.absoluteString\n            }\n            .disposed(by: disposeBag)\n    }\n}\n```\n\nThe documentation which does not use `KeyPath Dynamic Member Lookup` is [here](https://github.com/cats-oss/Unio/tree/0.4.1#about-unio).\n\n#### Migration Guides\n\n- [Unio 0.5.0 Migration Guide](./Documentation/Unio0_5_0MigrationGuide.md)\n- [Unio 0.6.0 Migration Guide](./Documentation/Unio0_6_0MigrationGuide.md)\n- [Unio 0.9.0 Migration Guide](./Documentation/Unio0_9_0MigrationGuide.md)\n\n### Xcode Template\n\nYou can use Xcode Templates for Unio. Let's install with `./Tools/install-xcode-template.sh` command!\n\n![](https://user-images.githubusercontent.com/2082134/54809497-cdb5f380-4cc6-11e9-97f1-a75bd4439891.png)\n\n![](https://user-images.githubusercontent.com/2082134/54809365-6f891080-4cc6-11e9-82c9-444b1fdefc07.gif)\n\n## Installation\n\n### Carthage\n\nIf you’re using [Carthage](https://github.com/Carthage/Carthage), simply add\nUnio to your `Cartfile`:\n\n```ruby\ngithub \"cats-oss/Unio\"\n```\n\n### CocoaPods\n\nUnio is available through [CocoaPods](https://cocoapods.org). To install\nit, simply add the following line to your Podfile:\n\n```ruby\npod \"Unio\"\n```\n\n### Swift Package Manager\n\nSimply add the following line to your `Package.swift`:\n\n```\n.package(url: \"https://github.com/cats-oss/Unio.git\", from: \"version\")\n```\n\n## Requirements\n\n- Swift 5 or greater\n- iOS 9.0 or greater\n- tvOS 10.0 or greater\n- watchOS 3.0 or greater\n- macOS 10.10 or greater\n- [RxSwift](https://github.com/ReactiveX/RxSwift) 6.0 or greater\n\n## License\n\nUnio is released under the MIT License.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcats-oss%2Funio","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcats-oss%2Funio","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcats-oss%2Funio/lists"}