[RxSwift]7.3、RxSwift 常用架构:ReactorKit

★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
➤微信公众号:山青咏芝(let_us_code)
➤博主域名:https://www.zengqiang.org
➤GitHub地址:https://github.com/strengthen/LeetCode
➤原文地址: https://www.cnblogs.com/strengthen/p/13581069.html
➤如果链接不是山青咏芝的博客园地址,则可能是爬取作者的文章。
➤原文已修改更新!强烈建议点击原文地址阅读!支持作者!支持原创!
★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★

热烈欢迎,请直接点击!!!

进入博主App Store主页,下载使用各个作品!!!

注:博主将坚持每月上线一个新app!!!

Swift CocoaPods Platform Build Status Codecov CocoaDocs

作者

Jeon Suyeol 是 ReactorKit 的作者。他也发布了一些富有创造性的框架,如 ThenURLNavigatorSwiftyImage 以及一些开源项目 RxTodoDrrrible。他也是多个组织的成员 RxSwiftCommunityMoyaSwiftKorea

介绍

ReactorKit 结合了 Flux 和响应式编程。用户行为和页面状态都是通过序列相互传递。这些序列都是单向的:页面只能发出用户行为,然而反应器(Reactor)只能发出状态。


View

View 用于展示数据。ViewController 和 Cell 都可以看作是 View。View 将用户输入绑定到 Action 的序列上,同时将页面状态绑定到 UI 组件上。

定义一个 View 只需要让它遵循 View 协议即可。然后你的类将自动获得一个 reactor 属性。这个属性应该在 View 的外面被设置:

class ProfileViewController: UIViewController, View {
  var disposeBag = DisposeBag()
}

profileViewController.reactor = UserViewReactor() // 注入 reactor

当 reactor 属性被设置时,bind(reactor:) 方法就会被调用。执行这个方法来进行用户输入绑定和状态输出绑定。

func bind(reactor: ProfileViewReactor) {
  // action (View -> Reactor)
  refreshButton.rx.tap.map { Reactor.Action.refresh }
    .bind(to: reactor.action)
    .disposed(by: self.disposeBag)

  // state (Reactor -> View)
  reactor.state.map { $0.isFollowing }
    .bind(to: followButton.rx.isSelected)
    .disposed(by: self.disposeBag)
}

Reactor

Reactor 是与 UI 相互独立的一层,主要负责状态管理。Reactor 最重要的作用就是将业务逻辑从 View 中抽离。每一个 View 都有对应的 Reactor 并且将所有的逻辑代理给 Reactor。Reactor 不需要依赖 View,所以它很容易被测试。

遵循 Reactor 协议即可定义一个 Reactor。这个协议需要定义三个类型:ActionMutation 和 State。它也需要一个 initialState 属性。

class ProfileViewReactor: Reactor {
  // 代表用户行为
  enum Action {
    case refreshFollowingStatus(Int)
    case follow(Int)
  }

  // 代表附加作用
  enum Mutation {
    case setFollowing(Bool)
  }

  // 代表页面状态
  struct State {
    var isFollowing: Bool = false
  }

  let initialState: State = State()
}

Action 代表用户行为,State 代表页面状态。Mutation 是 Action 和 State 的桥梁。Reactor 通过两步将用户行为序列转换为页面状态序列:mutate() 和 reduce()

mutate()

mutate() 接收一个 Action ,然后创建一个 Observable<Mutation>

func mutate(action: Action) -> Observable<Mutation>

每种附加作用,如,异步操作,API 调用都是在这个方法内执行。

func mutate(action: Action) -> Observable<Mutation> {
  switch action {
  case let .refreshFollowingStatus(userID): // receive an action
    return UserAPI.isFollowing(userID) // create an API stream
      .map { (isFollowing: Bool) -> Mutation in
        return Mutation.setFollowing(isFollowing) // convert to Mutation stream
      }

  case let .follow(userID):
    return UserAPI.follow()
      .map { _ -> Mutation in
        return Mutation.setFollowing(true)
      }
  }
}

reduce()

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

func reduce(state: State, mutation: Mutation) -> State

这个方法是一个纯函数。它将同步的返回一个 State。不会产生其他的作用。

func reduce(state: State, mutation: Mutation) -> State {
  var state = state // create a copy of the old state
  switch mutation {
  case let .setFollowing(isFollowing):
    state.isFollowing = isFollowing // manipulate the state, creating a new state
    return state // return the new state
  }
}

transform()

transform() 转换每一种序列。有三种转换方法:

func transform(action: Observable<Action>) -> Observable<Action>
func transform(mutation: Observable<Mutation>) -> Observable<Mutation>
func transform(state: Observable<State>) -> Observable<State>

执行这些方法可以转换或者组合其他的序列。例如,transform(mutation:) 最适合用来组合一个全局事件,生成一个 Mutation 序列。

它也可用来做调试:

func transform(action: Observable<Action>) -> Observable<Action> {
  return action.debug("action") // Use RxSwift's debug() operator
}

示例

本节将用 Github Search 来演示如何使用 ReactorKit


Github Search(示例)

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

简介

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

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

Action

Action 用于描叙用户行为:

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

Mutation

Mutation 用于描状态变更:

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

State

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

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

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


mutate()

将 Action 转换为 Mutation:

func mutate(action: Action) -> Observable<Mutation> {
    switch action {
    case let .updateQuery(query):
      return Observable.concat([
        // 1) set current state's query (.setQuery)
        Observable.just(Mutation.setQuery(query)),

        // 2) call API and set repos (.setRepos)
        self.search(query: query, page: 1)
          // cancel previous request when the new `.updateQuery` action is fired
          .takeUntil(self.action.filter(isUpdateQueryAction))
          .map { Mutation.setRepos($0, nextPage: $1) },
      ])

    case .loadNextPage:
      guard !self.currentState.isLoadingNextPage else { return Observable.empty() } // prevent from multiple requests
      guard let page = self.currentState.nextPage else { return Observable.empty() }
      return Observable.concat([
        // 1) set loading status to true
        Observable.just(Mutation.setLoadingNextPage(true)),

        // 2) call API and append repos
        self.search(query: self.currentState.query, page: page)
          .takeUntil(self.action.filter(isUpdateQueryAction))
          .map { Mutation.appendRepos($0, nextPage: $1) },

        // 3) set loading status to false
        Observable.just(Mutation.setLoadingNextPage(false)),
      ])
    }
  }
  • 当用户输入一个新的搜索关键字时,就从服务器请求 repos,然后转换成更新 repos 事件(Mutation)。
  • 当用户触发加载下页时,就从服务器请求 repos,然后转换成添加 repos 事件。

reduce()

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

func reduce(state: State, mutation: Mutation) -> State {
  switch mutation {
  case let .setQuery(query):
    var newState = state
    newState.query = query
    return newState

  case let .setRepos(repos, nextPage):
    var newState = state
    newState.repos = repos
    newState.nextPage = nextPage
    return newState

  case let .appendRepos(repos, nextPage):
    var newState = state
    newState.repos.append(contentsOf: repos)
    newState.nextPage = nextPage
    return newState

  case let .setLoadingNextPage(isLoadingNextPage):
    var newState = state
    newState.isLoadingNextPage = isLoadingNextPage
    return newState
  }
}
  • setQuery 更新搜索关键字
  • setRepos 更新搜索结果,以及下一页页数
  • appendRepos 添加搜索结果,以及下一页页数
  • setLoadingNextPage 设置是否正在加载下一页

bind(reactor:)

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

func bind(reactor: GitHubSearchViewReactor) {
  // Action
  searchBar.rx.text
    .throttle(0.3, scheduler: MainScheduler.instance)
    .map { Reactor.Action.updateQuery($0) }
    .bind(to: reactor.action)
    .disposed(by: disposeBag)

  tableView.rx.contentOffset
    .filter { [weak self] offset in
      guard let `self` = self else { return false }
      guard self.tableView.frame.height > 0 else { return false }
      return offset.y + self.tableView.frame.height >= self.tableView.contentSize.height - 100
    }
    .map { _ in Reactor.Action.loadNextPage }
    .bind(to: reactor.action)
    .disposed(by: disposeBag)

  // State
  reactor.state.map { $0.repos }
    .bind(to: tableView.rx.items(cellIdentifier: "cell")) { indexPath, repo, cell in
      cell.textLabel?.text = repo
    }
    .disposed(by: disposeBag)

  // View
  tableView.rx.itemSelected
    .subscribe(onNext: { [weak self, weak reactor] indexPath in
      guard let `self` = self else { return }
      self.tableView.deselectRow(at: indexPath, animated: false)
      guard let repo = reactor?.currentState.repos[indexPath.row] else { return }
      guard let url = URL(string: "https://github.com/(repo)") else { return }
      let viewController = SFSafariViewController(url: url)
      self.present(viewController, animated: true, completion: nil)
    })
    .disposed(by: disposeBag)
}
  • 将用户更改输入关键字行为绑定到用户行为上
  • 将用户要求加载下一页行为绑定到用户行为上
  • 将搜索结果输出到列表页上
  • 当用户点击某一条搜索结果是,用 Safari 打开链接

整体结构

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

GitHubSearchViewReactor.swift

final class GitHubSearchViewReactor: Reactor {
  enum Action {
    case updateQuery(String?)
    case loadNextPage
  }

  enum Mutation {
    case setQuery(String?)
    case setRepos([String], nextPage: Int?)
    case appendRepos([String], nextPage: Int?)
    case setLoadingNextPage(Bool)
  }

  struct State {
    var query: String?
    var repos: [String] = []
    var nextPage: Int?
    var isLoadingNextPage: Bool = false
  }

  let initialState = State()

  func mutate(action: Action) -> Observable<Mutation> {
    switch action {
    case let .updateQuery(query):
      return Observable.concat([
        // 1) set current state's query (.setQuery)
        Observable.just(Mutation.setQuery(query)),

        // 2) call API and set repos (.setRepos)
        self.search(query: query, page: 1)
          // cancel previous request when the new `.updateQuery` action is fired
          .takeUntil(self.action.filter(isUpdateQueryAction))
          .map { Mutation.setRepos($0, nextPage: $1) },
      ])

    case .loadNextPage:
      guard !self.currentState.isLoadingNextPage else { return Observable.empty() } // prevent from multiple requests
      guard let page = self.currentState.nextPage else { return Observable.empty() }
      return Observable.concat([
        // 1) set loading status to true
        Observable.just(Mutation.setLoadingNextPage(true)),

        // 2) call API and append repos
        self.search(query: self.currentState.query, page: page)
          .takeUntil(self.action.filter(isUpdateQueryAction))
          .map { Mutation.appendRepos($0, nextPage: $1) },

        // 3) set loading status to false
        Observable.just(Mutation.setLoadingNextPage(false)),
      ])
    }
  }

  func reduce(state: State, mutation: Mutation) -> State {
    switch mutation {
    case let .setQuery(query):
      var newState = state
      newState.query = query
      return newState

    case let .setRepos(repos, nextPage):
      var newState = state
      newState.repos = repos
      newState.nextPage = nextPage
      return newState

    case let .appendRepos(repos, nextPage):
      var newState = state
      newState.repos.append(contentsOf: repos)
      newState.nextPage = nextPage
      return newState

    case let .setLoadingNextPage(isLoadingNextPage):
      var newState = state
      newState.isLoadingNextPage = isLoadingNextPage
      return newState
    }
  }

  ...
}

GitHubSearchViewController.swift

class GitHubSearchViewController: UIViewController, View {
  @IBOutlet var searchBar: UISearchBar!
  @IBOutlet var tableView: UITableView!

  var disposeBag = DisposeBag()

  override func viewDidLoad() {
    super.viewDidLoad()
    tableView.contentInset.top = 44 // search bar height
    tableView.scrollIndicatorInsets.top = tableView.contentInset.top
  }

  func bind(reactor: GitHubSearchViewReactor) {
    // Action
    searchBar.rx.text
      .throttle(0.3, scheduler: MainScheduler.instance)
      .map { Reactor.Action.updateQuery($0) }
      .bind(to: reactor.action)
      .disposed(by: disposeBag)

    tableView.rx.contentOffset
      .filter { [weak self] offset in
        guard let `self` = self else { return false }
        guard self.tableView.frame.height > 0 else { return false }
        return offset.y + self.tableView.frame.height >= self.tableView.contentSize.height - 100
      }
      .map { _ in Reactor.Action.loadNextPage }
      .bind(to: reactor.action)
      .disposed(by: disposeBag)

    // State
    reactor.state.map { $0.repos }
      .bind(to: tableView.rx.items(cellIdentifier: "cell")) { indexPath, repo, cell in
        cell.textLabel?.text = repo
      }
      .disposed(by: disposeBag)

    // View
    tableView.rx.itemSelected
      .subscribe(onNext: { [weak self, weak reactor] indexPath in
        guard let `self` = self else { return }
        self.tableView.deselectRow(at: indexPath, animated: false)
        guard let repo = reactor?.currentState.repos[indexPath.row] else { return }
        guard let url = URL(string: "https://github.com/(repo)") else { return }
        let viewController = SFSafariViewController(url: url)
        self.present(viewController, animated: true, completion: nil)
      })
      .disposed(by: disposeBag)
  }
}

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

原文地址:https://www.cnblogs.com/strengthen/p/13581069.html