美文网首页iOS移动开发
函数响应式编程与RxSwift

函数响应式编程与RxSwift

作者: icetime17 | 来源:发表于2018-06-05 07:00 被阅读14次

    函数式编程

    本文介绍了函数响应式编程(FRP)以及 RxSwift 的一些内容, 源自公司内部的一次分享.

    不变状态(immutable state)与没有副作用(lack of side effects)

    通常,一个函数尽量不要修改外部的一些变量。
    函数的返回值有唯一性。

    行动式思维 VS 采用声明式思维

    - (MTFilterInfoModel *)filterInfoByFilterID:(NSInteger)filterID
                                        ofTheme:(NSString *)themeNumber {
        if (themeNumber) {
            NSArray <MTFilterInfoModel *> *filterModels = [self filterInfosByThemeNumber:themeNumber];
            for (MTFilterInfoModel *filter in filterModels) {
                if (filter.filterID == filterID) {
                    return filter;
                }
            }
        }
        return nil;
    }
    

    vs

    let filters: [MTFilterInfoModel] = filterModels.filter { filter in
        return filter.filterID == filterID
    }
    

    Array的filter函数可以接收一个闭包Closure类型的参数。

    对数组中的每个元素都执行一遍该Closure,根据Closure的返回值决定是否将该元素作为符合条件的元素放入查找结果(也是一个Array)中。

    Objective-C中可以使用enumerateObjectsUsingBlock。

    *** 注重Action VS 注重Result ***

    first class function, closure

    func myFilter(filter: MTFilterInfoModel) -> Bool {
        return filter.filterID == "5008"
    }
    let filters: [MTFilterInfoModel] = filterModels.filter(myFilter)
    

    OC中的 blocks 或 enumeratexxx 也可以做到。

    在Swift中使用高阶函数(map,reduce,filter等)。避免使用loop或enumeratexxx

    Swift的高阶函数使得其比Objective-C更适于函数式编程。

    柯里化

    就是把一个函数的多个参数分解成多个函数,然后把函数多层封装起来,每层函数都返回一个函数去接收下一个参数。

    即:用函数生成另一个函数

    “Swift 里可以将方法进行柯里化 (Currying),也就是把接受多个参数的方法变换成接受第一个参数的方法,并且返回接受余下的参数并且返回结果的新方法。”

    // currying
    func greaterThan(_ comparer: Int) -> (Int) -> Bool {
        return { $0 > comparer }
    }
    
    let isGreaterThan10 = greaterThan(10);
    
    print(isGreaterThan10(2))
    print(isGreaterThan10(20))
    

    参考资料

    FRP iOS Learning resources.md

    函数式Swift - 王巍

    异步编程

    OC中的链式代码

    Masonry的写法:

    如B+中的StillCameraViewController:

    [circleLoadingView mas_makeConstraints:^ (MASConstraintMaker *maker) {
                maker.leading.equalTo(thumbBottom).with.offset(30);
                maker.top.equalTo(thumbBottom);
                maker.width.equalTo(thumbBottom);
                maker.height.equalTo(thumbBottom);
            }];
    

    原理:

    - (MASConstraint * (^)(id))equalTo {
        return ^id(id attribute) {
            return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
        };
    }
    
    - (MASConstraint * (^)(CGFloat))offset {
        return ^id(CGFloat offset){
            self.offset = offset;
            return self;
        };
    }
    

    自己实现一个:

    @implementation Person
    
    - (Person * (^)(NSString *))named {
        return ^id(NSString *name) {
            self.name = name;
            return self;
        };
    }
    
    - (Person * (^)(NSInteger))withAge {
        return ^id(NSInteger age) {
            self.age = age;
            return self;
        };
    }
    
    - (Person * (^)(NSString *))liveIn {
        return ^id(NSString *city) {
            self.city = city;
            return self;
        };
    }
    
    @end
    

    使用如下:

    Person *p = [[Person alloc] init];
    [p.named(@"MyName").withAge(18).liveIn(@"Xiamen") doSomething];
    

    请求指定网络图片

    登录API -> 判断token值 -> 请求API获取真实的JSON数据 -> 解析得到图片URL -> 请求图片 -> 填充UIImageView

    [self request:api_login success:^{
        if (isTokenCorrect) {
            [self request:api_json success:^(NSData *data) {
                NSDictionary *json = [self parse:data];
                NSString *imgURL = json[@"thumbnail"];
                [SDWebImageHelper requestImg:imgURL success:^(UIImage *image, NSError *error) {
                    runInMainQueue {
                        self.imageView.image = image;
                    }
                }];
            }];
        }
    }];
    

    异步代码,线程切换。

    可以使用类似 Promise 的方式解决异步代码问题。

    状态更新

    Target-Action

    Delegate

    KVO

    Notification

    Blocks

    以上是Objective-C中的几种状态更新方式。

    响应式编程

    Reactive programming is programming with asynchronous data streams.

    响应式编程与以上的几种状态更新方式不同,关键在于 *** 将异步可观察序列对象模型化 *** 。

    命令式编码-Pull,响应式编程-Push。

    Push的内容即为异步数据流。

    而函数式编程可以非常方便地对数据流进行合并、创建、过滤、加工等操作,因此与响应式编程结合比较合适。

    RxSwift

    *** Function programming + Reactive programming + Swift -> RxSwift ***

    Why

    是时候学习 RxSwift 了

    rx

    btnClose.rx.tap
    

    自己构造一个类似的

    struct MT<Base> {
        let base: Base
    
        init(_ base: Base) {
            self.base = base
        }
    }
    
    protocol MTProtocol {
        associatedtype CompatibleType
    
        var mt: MT<CompatibleType> { get set }
    }
    
    extension MTProtocol {
        var mt: MT<Self> {
            get {
                return MT(self)
            }
            set {
    
            }
        }
    }
    
    extension NSObject: MTProtocol {}
    
    extension MT where Base: UIViewController {
        var size: CGSize {
            get {
                return base.view.frame.size
            }
        }
    }
    

    使用如下:

    print(viewController.mt.size)
    

    使用样例 1

    RxSwift Workflow

    这里引用limboy博客中的一张图:

    RxSwift Workflow

    简单的计算界面

    number1与number2为两个UITextField

    // 将两个Observable绑定在一起,构成一个Observable
    Observable.combineLatest(number1.rx.text, number2.rx.text) { (num1, num2) -> Int in
        if let num1 = num1, num1 != "", let num2 = num2, num2 != "" {
            return Int(num1)! + Int(num2)!
        } else {
            return 0
        }
    }
    // Observable发送的消息为Int,不能与result.rx.text绑定,所以需使用map进行映射
    .map { $0.description }
    // Obsever为result.rx.text
    .bindTo(result.rx.text)
    .addDisposableTo(CS_DisposeBag)
    

    注册登录界面

    // 声明Observable,可观察对象
    // username的text没有太多参考意义,因此使用map来加工,得到是否可用的消息
    let userValidation = textFieldUsername.rx.text.orEmpty
        // map的参数是一个closure,接收element
        .map { (user) -> Bool in
            let length = user.characters.count
            return length >= minUsernameLength && length <= maxUsernameLength
        }
        .shareReplay(1)
    
    let passwdValidataion = textFieldPasswd.rx.text.orEmpty
        .map{ (passwd) -> Bool in
            let length = passwd.characters.count
            return length >= minUsernameLength && length <= maxUsernameLength
        }
        .shareReplay(1)
    
    // 声明Observable
    // 组合两个Observable
    let loginValidation = Observable.combineLatest(userValidation, passwdValidataion) {
            $0 && $1
        }
        .shareReplay(1)
    
    
    // bind,即将Observable与Observer绑定,最终也会调用subscribe
    // 此处是将isEnabled视为一个Observer,接收userValidation的消息,做出响应
    // 所以Observable发送的消息与Observer能接收的消息要对应起来(此处是Bool)
    userValidation
        .bindTo(textFieldPasswd.rx.isEnabled)
        .addDisposableTo(CS_DisposeBag)
    userValidation
        .bindTo(lbUsernameInfo.rx.isHidden)
        .addDisposableTo(CS_DisposeBag)
    
    passwdValidataion
        .bindTo(lbPasswdInfo.rx.isHidden)
        .addDisposableTo(CS_DisposeBag)
    
    loginValidation
        .bindTo(btnLogin.rx.isEnabled)
        .addDisposableTo(CS_DisposeBag)
    

    使用样例 2

    监控UIScrollView的scroll操作。

    通常:UIScrollViewDelegate

    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        // scrollView 1:
            //
        // scrollView 2:
            //
        // scrollView 3:
            //
    }
    

    通过RxSwift:

    根本:scrollView的contentOffset在变化

    tableView.rx.contentOffset
                .map { $0.y }
                .subscribe(onNext: { (contentOffset) in
                    if contentOffset >= -UIApplication.shared.statusBarFrame.height / 2 {
                        UIApplication.shared.statusBarStyle = .lightContent
                    } else {
                        UIApplication.shared.statusBarStyle = .default
                    }
                })
                .addDisposableTo(CS_DisposeBag)
    

    使用样例 3

    对于UITextField, UISearchController,UIButton等等,常见的使用步骤如下:

    init

    setup Delegate,or addTargetxxx

    Delegate callback

    而使用RxSwift,则可以做到 *** 高聚合,低耦合 ***

    btnClose.rx.tap
        .subscribe(onNext: { [weak self] in
            guard let strongSelf = self else { return }
            strongSelf.dismiss(animated: true, completion: nil)
        })
        .addDisposableTo(CS_DisposeBag)
    

    Rx基本概念

    reactivex.io

    发布-订阅

    Observable:

    可观察对象,可组合。(发射数据)

    next新的事件数据,complete事件序列的结束,error异常导致结束

    所以next可以多次调用,而complete只有最后一次。

    /// Type that can be converted to observable sequence (`Observer<E>`).
    public protocol ObservableConvertibleType {
        /// Type of elements in sequence.
        associatedtype E
    
        /// Converts `self` to `Observable` sequence.
        ///
        /// - returns: Observable sequence that represents `self`.
        func asObservable() -> Observable<E>
    }
    

    此外,还有 *** create,just,of,from *** 等一系列函数

    from: Converts an array to an observable sequence.

    Observer

    对Observable发射的数据或数据序列做出响应,做出特定的操作。

    /// Supports push-style iteration over an observable sequence.
    public protocol ObserverType {
        /// The type of elements in sequence that observer can observe.
        associatedtype E
    
        /// Notify observer about sequence event.
        ///
        /// - parameter event: Event that occured.
        func on(_ event: Event<E>)
    }
    

    subscribe

    订阅事件。

    对事件序列中的事件,如next,complete,error进行响应,

    extension ObservableType {
        /**
        Subscribes an element handler, an error handler, a completion handler and disposed handler to an observable sequence.
    
        - parameter onNext: Action to invoke for each element in the observable sequence.
        - parameter onError: Action to invoke upon errored termination of the observable sequence.
        - parameter onCompleted: Action to invoke upon graceful termination of the observable sequence.
        - parameter onDisposed: Action to invoke upon any type of termination of sequence (if the sequence has
            gracefully completed, errored, or if the generation is cancelled by disposing subscription).
        - returns: Subscription object used to unsubscribe from the observable sequence.
        */
        public func subscribe(onNext: ((E) -> Void)? = nil, onError: ((Swift.Error) -> Void)? = nil, onCompleted: (() -> Void)? = nil, onDisposed: (() -> Void)? = nil)
            -> Disposable {
                xxx
            }
    }
    

    所以:

    *** Rx的关键在于Observer订阅Observable,Observable将数据push给Observer,Observer自己做出对应的响应。 ***

    map

    进行数据映射

    extension ObservableType {
    
        /**
            Projects each element of an observable sequence into a new form.
    
            - seealso: [map operator on reactivex.io](http://reactivex.io/documentation/operators/map.html)
    
            - parameter transform: A transform function to apply to each source element.
            - returns: An observable sequence whose elements are the result of invoking the transform function on each element of source.
    
            */
        public func map<R>(_ transform: @escaping (Self.E) throws -> R) -> RxSwift.Observable<R>
    
    

    bindTo

    extension ObservableType {
        /**
        Creates new subscription and sends elements to variable.
    
        In case error occurs in debug mode, `fatalError` will be raised.
        In case error occurs in release mode, `error` will be logged.
    
        - parameter variable: Target variable for sequence elements.
        - returns: Disposable object that can be used to unsubscribe the observer.
        */
        public func bindTo(_ variable: RxSwift.Variable<Self.E>) -> Disposable
    }
    

    Disposable

    定义了释放资源的统一行为。

    DisposeBag: 订阅会有Disposable,自动销毁相关的订阅。可简单类似autorelease机制

    BahaviorSubject 与 PublishSubject

    创建一个可添加新元素的Observable,让订阅对象能够接收包含初始值与新值的事件。

    BahaviorSubject代表了一个随时间推移而更新的值,包含初始值。

    let s = BehaviorSubject(value: "hello")
    // s.onNext("hello again") // 会替换到hello消息
    s.subscribe { // 不区分订阅事件,所以打印 next(hello)
        print($0)
    }
    // s.subscribe(onNext: { // 仅区分订阅事件,所以打印next事件接收的数据 hello
    //     print($0)
    // })
    .addDisposableTo(CS_DisposeBag)
    s.onNext("world") // 发送下一个事件
    s.onNext("!")
    s.onCompleted()
    s.onNext("??") // completed之后即不能响应了
    

    PublishSubject与BehaviorSubject类似,

    但PublishSubject不需要初始值,且不会将最后一个值发送给Observer。

    struct Person {
        let name = PublishSubject<String>()
        let age  = PublishSubject<Int>()
    }
    let person = Person()
    person.name.onNext("none")
    person.age.onNext(0)
    Observable.combineLatest(person.name, person.age) {
            "\($0) \($1)"
        }
        .debug()
        .subscribe {
            print($0)
        }
        .addDisposableTo(CS_DisposeBag)
    person.name.onNext("none again") // 该none again数据不会发送
    person.name.onNext("chris")
    person.age.onNext(18)
    person.name.onNext("ada")
    

    使用了combineLatest,则会等待需要combine的数据都准备好了才会发送。

    可以通过combineLatest来直观感受。

    使用PublishSubject的log如下:

    2017-06-22 16:46:51.160: AppDelegate.swift:186 (basicRx()) -> subscribed
    2017-06-22 16:46:51.161: AppDelegate.swift:186 (basicRx()) -> Event next(chris 18)
    next(chris 18)
    2017-06-22 16:46:51.162: AppDelegate.swift:186 (basicRx()) -> Event next(ada 18)
    next(ada 18)
    2017-06-22 16:46:51.162: AppDelegate.swift:165 (basicRx()) -> Event completed
    completed
    2017-06-22 16:46:51.162: AppDelegate.swift:165 (basicRx()) -> isDisposed
    

    operations

    可以通过combineLatest来直观感受。

    除了combine,还可以使用concat,merge,zip等到操作。

    zip需要两个元素都有新值才会发送数据。

    let personZip = Person()
    // zip需要两个元素都有新值才会发送
    Observable.zip(personZip.name, personZip.age) {
            "\($0) \($1)"
        }
        .subscribe {
            print($0)
        }
        .addDisposableTo(CS_DisposeBag)
    personZip.name.onNext("zip none") // 不会单独发送
    personZip.name.onNext("zip chris")// 放入序列中,等待age
    personZip.age.onNext(18)          // 结合zip none一起发送
    personZip.name.onNext("zip ada")  // 永远不会发送,在其之前已经有zip chris
    personZip.age.onNext(20)          // 结合zip chris一起发送
    personZip.name.onCompleted()
    personZip.age.onCompleted()
    

    打印的log如下:

    next(zip none 18)
    next(zip chris 20)
    completed
    2017-06-23 13:50:48.234: AppDelegate.swift:172 (basicRx()) -> Event completed
    completed
    2017-06-23 13:50:48.235: AppDelegate.swift:172 (basicRx()) -> isDisposed
    

    zip的场景要好好体会下,为何会是这两个输出。

    可以通过zip来直观感受。

    Variable

    Variable基于BahaviorSubject封装的类,通过asObservable()保留出其内部的BahaviorSubject的可观察序列。

    表示一个可监听的数据结构,可以监听数据变化,或者将其他值绑定到变量。

    Variable不会发生任何错误事件,即将被销毁处理的时候,会自动发送一个completed事件。因此有些使用Variable

    let v = Variable<String>("hello")
    v.asObservable()
        .debug()
        .distinctUntilChanged() // 消除连续重复的数据
        .subscribe {
            print($0)
        }
        .addDisposableTo(CS_DisposeBag)
    v.value = "world"
    v.value = "world" // 不会对重复的"world"做出响应
    v.value = "!"
    

    打印log如下,可以看出其发送的可观察序列:

    2017-06-22 16:22:57.208: AppDelegate.swift:162 (basicRx()) -> subscribed
    2017-06-22 16:22:57.211: AppDelegate.swift:162 (basicRx()) -> Event next(hello)
    next(hello)
    2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> Event next(world)
    next(world)
    2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> Event next(world)
    2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> Event next(!)
    next(!)
    2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> Event completed
    completed
    2017-06-22 16:22:57.212: AppDelegate.swift:162 (basicRx()) -> isDisposed
    

    其他

    如 *** Subject, BehaviorSubject, Driver 等等 ***

    RxSwiftStudy

    RxSwift学习博客

    RxSwift学习之旅 - Observable 和 Driver

    Demos

    Login

    *** RxSwift 与 RxCocoa ***

    两个Observable进行combine操作:

    let loginValidation = Observable.combineLatest(userValidation, passwdValidataion) {
                    $0 && $1
                }
                .shareReplay(1)
    

    构建UITableView

    RxDataSources

    对UITableView, UICollectionView的dataSource进行Rx的封装。

    设置数据源
    let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, User>>()
    
    声明configureCell
    dataSource.configureCell = xxx
    
    bind即可

    准备一个Observable<[SectionModel<String, User>]>,然后与tableView进行相关bind即可

    userViewModel.getUsers()
                .bindTo(tableView.rx.items(dataSource: dataSource))
                .addDisposableTo(CS_DisposeBag)
    
    点击操作
    tableView.rx
        .modelSelected(User.self)
        .subscribe(onNext: { user in
            print(user)
    
            let storyboard = UIStoryboard(name: "Main", bundle: nil)
            let loginVC = storyboard.instantiateViewController(withIdentifier: "LoginViewController")
            self.present(loginVC, animated: true, completion: nil)
        })
        .addDisposableTo(CS_DisposeBag)
    

    使用RxSwift来构建UICollectionView的步骤类似。

    SearchBar

    ViewModel如下:

    struct Repo {
        let name: String
        let url: String
    }
    
    class SearchBarViewModel {
    
        let searchText = Variable<String>("")
    
        let CS_DisposeBag = DisposeBag()
    
        lazy var repos: Driver<[Repo]> = {
            return self.searchText.asObservable()
                .throttle(0.3, scheduler: MainScheduler.instance)
                .distinctUntilChanged()
                .flatMapLatest { (user) -> Observable<[Repo]> in
                    if user.isEmpty {
                        return Observable.just([])
                    }
    
                    return self.searchRepos(user: user)
                }
                .asDriver(onErrorJustReturn: [])
        }()
    
        func searchRepos(user: String) -> Observable<[Repo]> {
            guard let url = URL(string: "https://api.github.com/users/\(user)/repos") else {
                return Observable.just([])
            }
    
            return URLSession.shared.rx.json(url: url)
                .retry(3)
                .debug()
                .map {
                    var repos = [Repo]()
    
                    if let items = $0 as? [[String: Any]] {
                        items.forEach {
                            guard let name = $0["name"] as? String,
                                  let url  = $0["url"]  as? String
                                else { return }
                            repos.append(Repo(name: name, url: url))
                        }
                    }
    
                    return repos
                }
        }
    
    }
    

    ViewController中的代码如下:

    var searchBarViewModel = SearchBarViewModel()
    
    tableView.tableHeaderView = searchVC.searchBar
    
    searchBar.rx.text.orEmpty
                .bindTo(searchBarViewModel.searchText)
                .addDisposableTo(CS_DisposeBag)
    
            searchBarViewModel.repos
                .drive(tableView.rx.items(cellIdentifier: "Cell", cellType: UITableViewCell.self)) { (row, repo, cell) in
                    cell.textLabel?.text = repo.name
                    cell.detailTextLabel?.text = repo.url
                }
                .addDisposableTo(CS_DisposeBag)
    

    UISearchBar中的text与ViewModel中的searchText进行绑定,

    而ViewModel中的searchText是Variable类型,作为Observable会在MainScheduler中输入间隔达到0.3s只后会触发调用searchRepos函数进行搜索。

    repos作为Driver,其中元素是包含Repo的数组。Driver同样封装了可观察序列,但Driver只在主线程执行。

    所以,做数据绑定可以使用bindTo和Driver,涉及到UI的绑定可以尽量使用Driver。

    在本例中,跟repos绑定的即是tableView.rx.items,即repos直接决定了tableView中的items展示内容。

    对应使用URLSession进行网络请求的场景,RxSwift也提供了非常方便的使用方式。注意各个地方Observable的类型保持一致即可。

    另外,注意 *** throttle *** , *** flatMapLatest *** 及 *** distinctUntilChanged *** 的用法。

    使用MVVM

    ViewModel

    优点

    数据绑定,精简Controller,便于单元测试。

    缺点

    数据绑定额外消耗,调试困难,

    *** 注意input与output即可 ***

    *** 难点在于如何合理的处理ViewModel与View的数据绑定问题。***

    如何写好一个ViewModel?

    此处关于写好ViewModel的建议,出自:

    【漫谈】从项目实践走向RxSwift响应式函数编程

    View不应该存在逻辑控制,只绑定展示数据而不对其做操作

    struct UserViewModel {
        let userName: String
        let userAge:  Int
        let userCity: String
    }
    
    textFieldUserName.rx.text.orEmpty
        .bindTo(userViewModel.userName)
    // 不推荐    
    textFiledUserAge.rx.text.orEmpty
        .map { Int($0) }
        .bindTo(userViewModel.userAge)
    

    View只能通过ViewModel知道View要做什么

    userViewModel.register()
    
    self.btnRegister.rx.tap
        .bindTo(userViewModel.register)
    

    ViewModel只暴露View显示所需要的最少信息

    struct UserViewModel {
        let user: UserModel
    }
    
    struct UserViewModel {
        let userName: String
        let userAge:  String
        let userCity: String
    }
    

    参考资料

    LearnRxSwift

    rx-sample-code

    RxSwift Reactive Programming with Swift by raywenderlich.com

    100-days-of-RxSwift

    RxSwift-CN

    iOS 架构模式 - 简述 MVC, MVP, MVVM 和 VIPER (译)

    相关文章

      网友评论

        本文标题:函数响应式编程与RxSwift

        本文链接:https://www.haomeiwen.com/subject/bwgusftx.html