Unidirectional Input Output 框架
介绍
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: InputWrapper
和output: OutputWrapper
属性。它可以自动从Input、State、Extra和Logic的实例生成input: InputWrapper
和output: 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许可证发布。