zoukankan      html  css  js  c++  java
  • [RxSwift]7.2、RxSwift 常用架构:RxFeedback

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

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

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

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

    作者

    Krunoslav Zaher 是 RxFeedback 的作者。他也是 RxSwift 的创始人以及 ReactiveX 组织 的核心成员。他有 16 年以上的编程经验( VR 引擎,BPM 系统,移动端应用程序,机器人等),最近在研究响应式编程。

    介绍

    RxSwift 最简单的架构

    typealias Feedback<State, Event> = (Observable<State>) -> Observable<Event>
    
    public static func system<State, Event>(
        initialState: State,
        reduce: @escaping (State, Event) -> State,
        feedback: Feedback<State, Event>...
    ) -> Observable<State>
    

    为什么?

    • 直接
      • 已经发生 -> Event
      • 即将发生 -> Request
      • 执行 Request -> Feedback loop
    • 声明式
      • 首先系统行为被明确声明出来,然后在调用 subscribe 后开始运作 => 编译时就保证了不会有“未处理状态”
    • 容易调试
      • 大多数逻辑是 纯函数,可以通过 xCode 调试器调试,或者将命令打印出来
    • 适用于任何级别
      • 整个系统
      • 应用程序(state 被储存在数据库中,CoreData, Firebase, Realm)
      • view controller (state 被储存在 system 操作符)
      • 在 feedback loop 中(feedback loop 中 调用另一个 system 操作符)
    • 容易做依赖注入
    • 易测试
      • Reducer 是 纯函数,只需调用他并断言结果即可
      • 伴随 附加作用 的测试 -> TestScheduler
    • 可以处理循环依赖
    • 完全从附加作用中分离业务逻辑
      • 业务逻辑可以在不同平台之间转换

    示例

    Observable.system(
        initialState: 0,
        reduce: { (state, event) -> State in
            switch event {
            case .increment:
                return state + 1
            case .decrement:
                return state - 1
            }
        },
        scheduler: MainScheduler.instance,
        feedback:
            // UI is user feedback
            bind(self) { me, state -> Bindings<Event> in
                let subscriptions = [
                    state.map(String.init).bind(to: me.label.rx.text)
                ]
    
                let events = [
                    me.plus.rx.tap.map { Event.increment },
                    me.minus.rx.tap.map { Event.decrement }
                ]
    
                return Bindings(
                    subscriptions: subscriptions,
                    events: events
                )
            }
    )
    

    这是一个简单计数的例子,只是用于演示 RxFeedback 架构。

    State

    系统状态用 State 表示:

    typealias State = Int
    
    • 这里的状态就是计数的数值

    Event

    事件用 Event 表示:

    enum Event {
        case increment
        case decrement
    }
    
    • increment 增加数值事件
    • decrement 减少数值事件

    当产生 Event 时更新状态:

    Observable.system(
        initialState: 0,
        reduce: { (state, event) -> State in
                switch event {
                case .increment:
                    return state + 1
                case .decrement:
                    return state - 1
                }
            },
        scheduler: MainScheduler.instance,
        feedback: ...
        )
    
    • increment 状态数值加一
    • decrement 状态数值减一

    Feedback Loop

    将状态输出到 UI 页面上,或者将 UI 事件输入到反馈循环里面去:

    Observable.system(
        initialState: 0,
        reduce: { ... },
        scheduler: MainScheduler.instance,
        feedback:
            // UI is user feedback
            bind(self) { me, state -> Bindings<Event> in
                let subscriptions = [
                    state.map(String.init).bind(to: me.label.rx.text)
                ]
    
                let events = [
                    me.plus.rx.tap.map { Event.increment },
                    me.minus.rx.tap.map { Event.decrement }
                ]
    
                return Bindings(
                    subscriptions: subscriptions,
                    events: events
                )
            }
        )
    
    • 将状态数值用 label 显示出来
    • 将增加按钮的点击,作为增加数值事件传入
    • 将减少按钮的点击,作为减少数值事件传入

    安装

    CocoaPods

    CocoaPods 是一个 Cocoa 项目的依赖管理工具。你可以通过以下命令安装他:

    $ gem install cocoapods
    

    将 RxFeedback 整合到项目中来,你需要在 Podfile 中指定他:

    pod 'RxFeedback', '~> 3.0'
    

    然后运行以下命令:

    $ pod install
    

    Carthage

    Carthage 是一个分散式依赖管理工具,他将构建你的依赖并提供二进制框架。

    你可以通过以下 Homebrew 命令安装 Carthage:

    $ brew update
    $ brew install carthage
    

    将 RxFeedback 整合到项目中来,你需要在 Cartfile 中指定他:

    github "NoTests/RxFeedback" ~> 3.0
    

    运行 carthage update 去构建框架,然后将 RxFeedback.framework 拖入到 Xcode 项目中来。由于 RxFeedback 对 RxSwift 和 RxCocoa 有依赖,所以你也需要将 RxSwift.framework 和 RxCocoa.framework 拖入到 Xcode 项目中来。

    Swift Package Manager

    Swift Package Manager 是一个自动分发 Swift 代码的工具,他已经被集成到 Swift 编译器中。

    一旦你配置好了 Swift 包,添加 RxFeedback 就非常简单了,你只需要将他添加到文件 Package.swift 的 dependencies 的值中。

    dependencies: [
        .package(url: "https://github.com/NoTests/RxFeedback.swift.git", majorVersion: 1)
    ]
    

    与其他架构的区别

    • Elm - 非常相似,feedback loop 用作 附加作用, 而不是 Cmd, 要执行的 附加作用 被编码到 state 中,并且通过 feedback loop 完成请求
    • Redux - 也很像,不过采用 feedback loops 而不是 middleware
    • Redux-Observable - observables 观察状态,与视图和状态之间的 middleware
    • Cycle.js - 一言难尽 :),请咨询 @andrestaltz
    • MVVM - 将状态和 附加作用 分离,而且不需要 View

    示例

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

    Github Search(示例)

    这个例子是我们经常会遇见的Github 搜索。它是使用 RxFeedback 重构以后的版本,你可以在这里下载这个例子

    简介

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

    • 输入搜索关键字,显示搜索结果
    • 当请求时产生错误,就给出错误提示
    • 当用户滑动列表到底部时,加载下一页

    State

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

    fileprivate struct State {
        var search: String {
            didSet { ... }
        }
        var nextPageURL: URL?
        var shouldLoadNextPage: Bool
        var results: [Repository]
        var lastError: GitHubServiceError?
    }
    
    ...
    
    extension State {
        var loadNextPage: URL? { return ... }
    }
    

    我们这个例子(Github 搜索) 就有这样几个状态:

    • search 搜索关键字
    • nextPageURL 下一页的 URL
    • shouldLoadNextPage 是否可以加载下一页
    • results 搜索结果
    • lastError 搜索时产生的错误
    • loadNextPage 加载下一页的触发

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

    或者,用被请求的状态,触发另外一个事件。


    Event

    这个是用于描述所产生的事件:

    fileprivate enum Event {
        case searchChanged(String)
        case response(SearchRepositoriesResponse)
        case startLoadingNextPage
    }
    

    事件通常会使状态发生变化,然后产生一个新的状态:

    extension State {
        ...
        static func reduce(state: State, event: Event) -> State {
            switch event {
            case .searchChanged(let search):
                var result = state
                result.search = search
                result.results = []
                return result
            case .startLoadingNextPage:
                var result = state
                result.shouldLoadNextPage = true
                return result
            case .response(.success(let response)):
                var result = state
                result.results += response.repositories
                result.shouldLoadNextPage = false
                result.nextPageURL = response.nextURL
                result.lastError = nil
                return result
            case .response(.failure(let error)):
                var result = state
                result.shouldLoadNextPage = false
                result.lastError = error
                return result
            }
        }
    }
    

    当发生某个事件时,更新当前状态:

    • searchChanged 搜索关键字变更

      将搜索关键字更新成当前值,并且清空搜索结果。

    • startLoadingNextPage 触发加载下页

      允许加载下一页,如果下一页的 URL 存在,就加载下一页。

    • response(.success(...)) 搜索结果返回成功

      将搜索结果加入到对应的数组里面去,然后将相关状态更新。

    • response(.failure(...)) 搜索结果返回失败

      保存错误状态。


    Feedback Loop

    Feedback Loop 是用来引入附加作用的。

    例如,你可以将状态输出到 UI 页面上,或者将 UI 事件输入到反馈循环里面去:

    override func viewDidLoad() {
        super.viewDidLoad()
    
        ...
    
        Driver.system(
            initialState: State.empty,
            reduce: State.reduce,
            feedback:
            // UI, user feedback
            UI.bind(self) { me, state in
                let subscriptions = [
                    state.map { $0.search }.drive(me.searchText!.rx.text),
                    state.map { $0.lastError?.displayMessage }.drive(me.status!.rx.textOrHide),
                    state.map { $0.results }.drive(searchResults.rx.items(cellIdentifier: "repo"))(configureRepository),
                    state.map { $0.loadNextPage?.description }.drive(me.loadNextPage!.rx.textOrHide),
                    ]
                let events = [
                    me.searchText!.rx.text.orEmpty.changed.asDriver().map(Event.searchChanged),
                    triggerLoadNextPage(state)
                ]
                return UI.Bindings(subscriptions: subscriptions, events: events)
            },
            // NoUI, automatic feedback
            ...
            )
            .drive()
            .disposed(by: disposeBag)
    }
    

    这里定义的 subscriptions 就是如何将状态输出到 UI 页面上,而 events 则是如何将 UI 事件输入到反馈循环里面去。


    被请求的状态

    被请求的状态是,用于发出异步请求,以事件的形式返回结果。

    override func viewDidLoad() {
        super.viewDidLoad()
        ...
    
        Driver.system(
            initialState: State.empty,
            reduce: State.reduce,
            feedback:
            // UI, user feedback
            ... ,
            // NoUI, automatic feedback
            react(query: { $0.loadNextPage }, effects: { resource in
                return URLSession.shared.loadRepositories(resource: resource)
                    .asDriver(onErrorJustReturn: .failure(.offline))
                    .map(Event.response)
            })
            )
            .drive()
            .disposed(by: disposeBag)
    }
    

    这里 loadNextPage 就是被请求的状态,当状态 loadNextPage 不为 nil 时,就请求加载下一页。


    整体结构

    现在我们看一下这个例子整体结构,这样可以帮助你理解这种架构。然后,以下是核心代码:

    ...
    fileprivate struct State {
        var search: String {
            didSet {
                if search.isEmpty {
                    self.nextPageURL = nil
                    self.shouldLoadNextPage = false
                    self.results = []
                    self.lastError = nil
                    return
                }
                self.nextPageURL = URL(string: "https://api.github.com/search/repositories?q=(search.URLEscaped)")
                self.shouldLoadNextPage = true
                self.lastError = nil
            }
        }
    
        var nextPageURL: URL?
        var shouldLoadNextPage: Bool
        var results: [Repository]
        var lastError: GitHubServiceError?
    }
    
    fileprivate enum Event {
        case searchChanged(String)
        case response(SearchRepositoriesResponse)
        case startLoadingNextPage
    }
    
    // transitions
    extension State {
        static var empty: State {
            return State(search: "", nextPageURL: nil, shouldLoadNextPage: true, results: [], lastError: nil)
        }
        static func reduce(state: State, event: Event) -> State {
            switch event {
            case .searchChanged(let search):
                var result = state
                result.search = search
                result.results = []
                return result
            case .startLoadingNextPage:
                var result = state
                result.shouldLoadNextPage = true
                return result
            case .response(.success(let response)):
                var result = state
                result.results += response.repositories
                result.shouldLoadNextPage = false
                result.nextPageURL = response.nextURL
                result.lastError = nil
                return result
            case .response(.failure(let error)):
                var result = state
                result.shouldLoadNextPage = false
                result.lastError = error
                return result
            }
        }
    }
    
    // queries
    extension State {
        var loadNextPage: URL? {
            return self.shouldLoadNextPage ? self.nextPageURL : nil
        }
    }
    
    class GithubPaginatedSearchViewController: UIViewController {
        @IBOutlet weak var searchText: UISearchBar?
        @IBOutlet weak var searchResults: UITableView?
        @IBOutlet weak var status: UILabel?
        @IBOutlet weak var loadNextPage: UILabel?
    
        private let disposeBag = DisposeBag()
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            let searchResults = self.searchResults!
    
            searchResults.register(UITableViewCell.self, forCellReuseIdentifier: "repo")
    
            let triggerLoadNextPage: (Driver<State>) -> Driver<Event> = { state in
                return state.flatMapLatest { state -> Driver<Event> in
                    if state.shouldLoadNextPage {
                        return Driver.empty()
                    }
    
                    return searchResults.rx.nearBottom.map { _ in Event.startLoadingNextPage }
                }
            }
    
            func configureRepository(_: Int, repo: Repository, cell: UITableViewCell) {
                cell.textLabel?.text = repo.name
                cell.detailTextLabel?.text = repo.url.description
            }
    
            let bindUI: (Driver<State>) -> Driver<Event> = UI.bind(self) { me, state in
                let subscriptions = [
                    state.map { $0.search }.drive(me.searchText!.rx.text),
                    state.map { $0.lastError?.displayMessage }.drive(me.status!.rx.textOrHide),
                    state.map { $0.results }.drive(searchResults.rx.items(cellIdentifier: "repo"))(configureRepository),
                    state.map { $0.loadNextPage?.description }.drive(me.loadNextPage!.rx.textOrHide),
                    ]
                let events = [
                    me.searchText!.rx.text.orEmpty.changed.asDriver().map(Event.searchChanged),
                    triggerLoadNextPage(state)
                ]
                return UI.Bindings(subscriptions: subscriptions, events: events)
            }
    
            Driver.system(
                initialState: State.empty,
                reduce: State.reduce,
                feedback:
                // UI, user feedback
                bindUI,
                // NoUI, automatic feedback
                react(query: { $0.loadNextPage }, effects: { resource in
                    return URLSession.shared.loadRepositories(resource: resource)
                        .asDriver(onErrorJustReturn: .failure(.offline))
                        .map(Event.response)
                })
                )
                .drive()
                .disposed(by: disposeBag)
        }
    }
    ...
    

    这是使用 RxFeedback 重构以后的 Github Search。你可以对比一下使用 ReactorKit 重构以后的 Github Search 两者有许多相似之处。

  • 相关阅读:
    理解Objective-C Runtime (六)super
    理解Objective-C Runtime (五)协议与分类
    理解Objective-C Runtime(四)Method Swizzling
    理解Objective-C Runtime(三)消息转发机制
    Objective-C Runtime(二)消息传递机制
    matlab数学实验--第一章
    Python之json模块
    Python之os模块和sys模块
    Python之小练习
    vuedevtools 离线安装
  • 原文地址:https://www.cnblogs.com/strengthen/p/13581055.html
Copyright © 2011-2022 走看看