Github Search(示例)

Github Search(示例) - 图1

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

简介

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

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

Action

Action 用于描叙用户行为:

  1. enum Action {
  2. case updateQuery(String?)
  3. case loadNextPage
  4. }
  • updateQuery 搜索关键字变更
  • loadNextPage 触发加载下页

Mutation

Mutation 用于描状态变更:

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

State

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

  1. struct State {
  2. var query: String?
  3. var repos: [String] = []
  4. var nextPage: Int?
  5. var isLoadingNextPage: Bool = false
  6. }
  • query 搜索关键字
  • repos 搜索结果
  • nextPage 下一页页数
  • isLoadingNextPage 是否正在加载下一页

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


mutate()

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. .map { Mutation.appendRepos($0, nextPage: $1) },
  23. // 3) set loading status to false
  24. Observable.just(Mutation.setLoadingNextPage(false)),
  25. ])
  26. }
  27. }
  • 当用户输入一个新的搜索关键字时,就从服务器请求 repos,然后转换成更新 repos 事件(Mutation)。
  • 当用户触发加载下页时,就从服务器请求 repos,然后转换成添加 repos 事件。

reduce()

reduce() 通过旧的 State 以及 Mutation 创建一个新的 State

  1. func reduce(state: State, mutation: Mutation) -> State {
  2. switch mutation {
  3. case let .setQuery(query):
  4. var newState = state
  5. newState.query = query
  6. return newState
  7. case let .setRepos(repos, nextPage):
  8. var newState = state
  9. newState.repos = repos
  10. newState.nextPage = nextPage
  11. return newState
  12. case let .appendRepos(repos, nextPage):
  13. var newState = state
  14. newState.repos.append(contentsOf: repos)
  15. newState.nextPage = nextPage
  16. return newState
  17. case let .setLoadingNextPage(isLoadingNextPage):
  18. var newState = state
  19. newState.isLoadingNextPage = isLoadingNextPage
  20. return newState
  21. }
  22. }
  • setQuery 更新搜索关键字
  • setRepos 更新搜索结果,以及下一页页数
  • appendRepos 添加搜索结果,以及下一页页数
  • setLoadingNextPage 设置是否正在加载下一页

bind(reactor:)

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

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

整体结构

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

GitHubSearchViewReactor.swift

  1. final class GitHubSearchViewReactor: Reactor {
  2. enum Action {
  3. case updateQuery(String?)
  4. case loadNextPage
  5. }
  6. enum Mutation {
  7. case setQuery(String?)
  8. case setRepos([String], nextPage: Int?)
  9. case appendRepos([String], nextPage: Int?)
  10. case setLoadingNextPage(Bool)
  11. }
  12. struct State {
  13. var query: String?
  14. var repos: [String] = []
  15. var nextPage: Int?
  16. var isLoadingNextPage: Bool = false
  17. }
  18. let initialState = State()
  19. func mutate(action: Action) -> Observable<Mutation> {
  20. switch action {
  21. case let .updateQuery(query):
  22. return Observable.concat([
  23. // 1) set current state's query (.setQuery)
  24. Observable.just(Mutation.setQuery(query)),
  25. // 2) call API and set repos (.setRepos)
  26. self.search(query: query, page: 1)
  27. // cancel previous request when the new `.updateQuery` action is fired
  28. .takeUntil(self.action.filter(isUpdateQueryAction))
  29. .map { Mutation.setRepos($0, nextPage: $1) },
  30. ])
  31. case .loadNextPage:
  32. guard !self.currentState.isLoadingNextPage else { return Observable.empty() } // prevent from multiple requests
  33. guard let page = self.currentState.nextPage else { return Observable.empty() }
  34. return Observable.concat([
  35. // 1) set loading status to true
  36. Observable.just(Mutation.setLoadingNextPage(true)),
  37. // 2) call API and append repos
  38. self.search(query: self.currentState.query, page: page)
  39. .takeUntil(self.action.filter(isUpdateQueryAction))
  40. .map { Mutation.appendRepos($0, nextPage: $1) },
  41. // 3) set loading status to false
  42. Observable.just(Mutation.setLoadingNextPage(false)),
  43. ])
  44. }
  45. }
  46. func reduce(state: State, mutation: Mutation) -> State {
  47. switch mutation {
  48. case let .setQuery(query):
  49. var newState = state
  50. newState.query = query
  51. return newState
  52. case let .setRepos(repos, nextPage):
  53. var newState = state
  54. newState.repos = repos
  55. newState.nextPage = nextPage
  56. return newState
  57. case let .appendRepos(repos, nextPage):
  58. var newState = state
  59. newState.repos.append(contentsOf: repos)
  60. newState.nextPage = nextPage
  61. return newState
  62. case let .setLoadingNextPage(isLoadingNextPage):
  63. var newState = state
  64. newState.isLoadingNextPage = isLoadingNextPage
  65. return newState
  66. }
  67. }
  68. ...
  69. }

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

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