Unio 0.11.0

Unio 0.11.0

rinovYusuke Morishitamarty-suzukimarty-suzukiTaiki Suzuki 维护。



 
依赖项
RxSwift~> 6.0
RxRelay~> 6.0
 

Unio 0.11.0

  • Taiki Suzuki

Unidirectional Input Output 框架

Build Status License Platform
Carthage compatible Version Carthage compatible

介绍

MVVM 中的普通 ViewModels 可能这样实现。有两个输入,一个是来自外部的输入(func search(query:)),另一个是内部输入中继(_search: PublishRelay)。如果可能表达“仅能在内部接收”和“仅能外部输入”的东西,这两个输入可以合并为一个。

此外,还有两个输出,一个是可观察属性(repositories: Observable<[Repository]> '),另一个是计算属性(repositoriesValue: [Repository])。这些输出与内部状态(_repositories: BehaviorRelay<[Repository]>)相关。如果可能表达“仅能在外部接收”和“仅能内部输入”的东西,这两个输出可以合并为一个。

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)
    }
}

关于 Unio

Unio 是基于 KeyPath 的 Unidirectional Input / Output 框架,与 RxSwift 一起工作。它通过使用这些组件解决上述问题

输入

Input 的规则是在内部作用域内定义 PublishRelay(或 PublishSubject)属性。

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

Input 的属性定义在内部作用域内。但如果 Input 用 InputWrapper 包装,则只能通过 KeyPath 访问 func accept(_:)(或 AnyObserver)。

let input: InputWrapper<Input>

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

输出

输出的规则是拥有BehaviorRelay(或BehaviorSubject等)属性,这些属性在内部作用域中定义。

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

输出的属性在内部作用域中定义。但是,只有当输出被OutputWrapper包装时,才能通过键路径访问func.asObservable()。

let output: OutputWrapper<Output>

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

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

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

如果属性是BehaviorRelay(或BehaviorSubject),则可以通过键路径访问值。

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

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

如果属性定义为Computed,则可以访问计算值。

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

状态

状态的规则是拥有UnioStream的内嵌状态。

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

额外

额外规则的规则是拥有UnioStream的其他依赖项。

struct Extra: ExtraType {
    let apiStream: GitHubSearchAPIStream
}

逻辑

逻辑的规则是从Dependency生成输出。它在UnioStream初始化时被调用一次。static func bind(from:disposeBag:)是从调用一次的函数。

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
}

在static func bind(from:disposeBag:)中连接序列并生成输出,以使用下面的属性和方法。

  • dependency.state
  • dependency.extra
  • dependency.inputObservables ... 返回一个Observable,它是输入的一个属性。
  • disposeBag ... 与UnioStream具有相同的生命周期。

以下是实现示例。

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表示MVVM(视图模型)的ViewModel(也可以用作模型)。它具有input: InputWrapperoutput: OutputWrapper属性。它可以自动从InputStateExtraLogic的实例生成input: InputWrapperoutput: OutputWrapper

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)
}

可以像这样定义UnioStream的子类。

final class GitHubSearchViewStream: UnioStream<GitHubSearchViewStream> {

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

用法

以下是一个示例。

定义GitHubSearchViewStream以搜索GitHub代码仓库。

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)
    }
}

将searchBar文本绑定到viewStream输入。另一方面,将viewStream输出绑定到tableView数据源。

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)
    }
}

未使用KeyPath Dynamic Member Lookup的文档在此处

迁移指南

Xcode模板

您可以使用Xcode模板进行Unio。使用./Tools/install-xcode-template.sh命令安装!

安装

使用Carthage

如果您正在使用Carthage,只需将Unio添加到您的Cartfile

github "cats-oss/Unio"

CocoaPods

Unio可通过CocoaPods获得。要安装它,只需将以下行添加到您的Podfile中

pod "Unio"

Swift包管理器

只需将以下行添加到您的Package.swift

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

需求

  • Swift 5或更高版本
  • iOS 9.0或更高版本
  • tvOS 10.0或更高版本
  • watchOS 3.0或更高版本
  • macOS 10.10或更高版本
  • RxSwift 5.0或更高版本

许可证

Unio遵循MIT许可证发布。