我们还是使用Github 搜索来演示如何使用 。这个例子是使用 ReactorKit 重构以后的版本,你可以在这里下载。

    这个 App 主要有这样几个交互:

    • 输入搜索关键字,显示搜索结果
    • 当用户滑动列表到底部时,加载下一页
    • 当用户点击某一条搜索结果是,用 Safari 打开链接

    Action

    Action 用于描叙用户行为:

    • updateQuery 搜索关键字变更
    • loadNextPage 触发加载下页

    1. case setQuery(String?)
    2. case setRepos([String], nextPage: Int?)
    3. case appendRepos([String], nextPage: Int?)
    4. case setLoadingNextPage(Bool)
    5. }
    • setQuery 更新搜索关键字
    • setRepos 更新搜索结果
    • appendRepos 添加搜索结果
    • setLoadingNextPage 设置是否正在加载下一页

    State

    这个是用于描述当前状态:

    • query 搜索关键字
    • repos 搜索结果
    • nextPage 下一页页数
    • isLoadingNextPage 是否正在加载下一页

    我们通常会使用这些状态来控制页面布局。


    Action 转换为 Mutation

    1. func mutate(action: Action) -> Observable<Mutation> {
    2. switch action {
    3. case let .updateQuery(query):
    4. return Observable.concat([
    5. // 1) set current state's query (.setQuery)
    6. Observable.just(Mutation.setQuery(query)),
    7. // 2) call API and set repos (.setRepos)
    8. self.search(query: query, page: 1)
    9. // cancel previous request when the new `.updateQuery` action is fired
    10. .takeUntil(self.action.filter(isUpdateQueryAction))
    11. .map { Mutation.setRepos($0, nextPage: $1) },
    12. ])
    13. case .loadNextPage:
    14. guard !self.currentState.isLoadingNextPage else { return Observable.empty() } // prevent from multiple requests
    15. guard let page = self.currentState.nextPage else { return Observable.empty() }
    16. return Observable.concat([
    17. // 1) set loading status to true
    18. Observable.just(Mutation.setLoadingNextPage(true)),
    19. // 2) call API and append repos
    20. self.search(query: self.currentState.query, page: page)
    21. .takeUntil(self.action.filter(isUpdateQueryAction))
    22. // 3) set loading status to false
    23. Observable.just(Mutation.setLoadingNextPage(false)),
    24. ])
    25. }
    26. }
    • 当用户输入一个新的搜索关键字时,就从服务器请求 repos,然后转换成更新 repos 事件(Mutation)。
    • 当用户触发加载下页时,就从服务器请求 repos,然后转换成添加 repos 事件。

    reduce()

    • setQuery 更新搜索关键字
    • setRepos 更新搜索结果,以及下一页页数
    • appendRepos 添加搜索结果,以及下一页页数
    • setLoadingNextPage 设置是否正在加载下一页

    View 层进行用户输入绑定和状态输出绑定:

    1. func bind(reactor: GitHubSearchViewReactor) {
    2. searchBar.rx.text
    3. .throttle(0.3, scheduler: MainScheduler.instance)
    4. .map { Reactor.Action.updateQuery($0) }
    5. .bind(to: reactor.action)
    6. .disposed(by: disposeBag)
    7. tableView.rx.contentOffset
    8. .filter { [weak self] offset in
    9. guard let `self` = self else { return false }
    10. guard self.tableView.frame.height > 0 else { return false }
    11. return offset.y + self.tableView.frame.height >= self.tableView.contentSize.height - 100
    12. }
    13. .map { _ in Reactor.Action.loadNextPage }
    14. .bind(to: reactor.action)
    15. .disposed(by: disposeBag)
    16. // State
    17. reactor.state.map { $0.repos }
    18. .bind(to: tableView.rx.items(cellIdentifier: "cell")) { indexPath, repo, cell in
    19. cell.textLabel?.text = repo
    20. }
    21. .disposed(by: disposeBag)
    22. // View
    23. tableView.rx.itemSelected
    24. .subscribe(onNext: { [weak self, weak reactor] indexPath in
    25. guard let `self` = self else { return }
    26. self.tableView.deselectRow(at: indexPath, animated: false)
    27. guard let repo = reactor?.currentState.repos[indexPath.row] else { return }
    28. guard let url = URL(string: "https://github.com/\(repo)") else { return }
    29. let viewController = SFSafariViewController(url: url)
    30. self.present(viewController, animated: true, completion: nil)
    31. })
    32. .disposed(by: disposeBag)
    33. }
    • 将用户更改输入关键字行为绑定到用户行为上
    • 将用户要求加载下一页行为绑定到用户行为上
    • 当用户点击某一条搜索结果是,用 Safari 打开链接

    整体结构

    我们已经了解 ReactorKit 每一个组件的功能了,现在我们看一下完整的核心代码:

    GitHubSearchViewReactor.swift

    1. class GitHubSearchViewController: UIViewController, View {
    2. @IBOutlet var searchBar: UISearchBar!
    3. @IBOutlet var tableView: UITableView!
    4. var disposeBag = DisposeBag()
    5. override func viewDidLoad() {
    6. tableView.contentInset.top = 44 // search bar height
    7. tableView.scrollIndicatorInsets.top = tableView.contentInset.top
    8. }
    9. func bind(reactor: GitHubSearchViewReactor) {
    10. // Action
    11. searchBar.rx.text
    12. .throttle(0.3, scheduler: MainScheduler.instance)
    13. .map { Reactor.Action.updateQuery($0) }
    14. .bind(to: reactor.action)
    15. .disposed(by: disposeBag)
    16. tableView.rx.contentOffset
    17. .filter { [weak self] offset in
    18. guard let `self` = self else { return false }
    19. guard self.tableView.frame.height > 0 else { return false }
    20. return offset.y + self.tableView.frame.height >= self.tableView.contentSize.height - 100
    21. }
    22. .map { _ in Reactor.Action.loadNextPage }
    23. .bind(to: reactor.action)
    24. .disposed(by: disposeBag)
    25. // State
    26. reactor.state.map { $0.repos }
    27. .bind(to: tableView.rx.items(cellIdentifier: "cell")) { indexPath, repo, cell in
    28. cell.textLabel?.text = repo
    29. }
    30. .disposed(by: disposeBag)
    31. // View
    32. tableView.rx.itemSelected
    33. .subscribe(onNext: { [weak self, weak reactor] indexPath in
    34. guard let `self` = self else { return }
    35. self.tableView.deselectRow(at: indexPath, animated: false)
    36. guard let repo = reactor?.currentState.repos[indexPath.row] else { return }
    37. guard let url = URL(string: "https://github.com/\(repo)") else { return }
    38. let viewController = SFSafariViewController(url: url)
    39. self.present(viewController, animated: true, completion: nil)
    40. })
    41. .disposed(by: disposeBag)
    42. }
    43. }

    这是使用 重构以后的 Github Search。ReactorKit 分层非常详细,分工也是非常明确的。当你在处理大型应用程序时,这可以帮助你更好的管理代码。