zoukankan      html  css  js  c++  java
  • [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 分层非常详细,分工也是非常明确的。当你在处理大型应用程序时,这可以帮助你更好的管理代码。

  • 相关阅读:
    Tensorflow io demo (待)
    tf.Dataset
    tf.estimator
    并发队列
    Callable的Future模式
    hadoop之HDFS介绍
    线程池
    并发工具类
    并发编程
    初学hadoop之hadoop集群搭建
  • 原文地址:https://www.cnblogs.com/strengthen/p/13581069.html
Copyright © 2011-2022 走看看