RxCocoa对于UI的绑定&&网络的处理

130 阅读4分钟

RxCocoa

RxCocoa适用于所有平台,针对每个平台的需求:iOSwatchOSiPadOStvOSmacOSMac Catalyst。每个平台都有一组自定义包装器,为许多UI控件和其他SDK类提供一组内置扩展。

下文中即将用到的UITextFieldUITextView都将使用RxCocoa

UITextFieldUITextView

UITextField的问题

UITextField赋值

先来看代码

func testTextFieldAndTextView () {

    _ = textFiled.rx.text.subscribe(onNext: { text in
       guard let str = text else { return }
       print("textField 收到\(str)")

    })

    _ = textView.rx.text.subscribe(onNext: { text in

        guard let str = text else { return }
        print("textView 收到\(str)")

    })

}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

    print("12345")
    textFiled.text = "field-123456"
    textView.text += "-123"

}


打印结果:

textField 收到
textView 收到view-123456

可以看到在初始化的时候二者都会有一次订阅的打印,但是在点击屏幕后,在对UITextFieldUITextView的赋值后,仅有UITextView有订阅的打印,而UITextField虽然控件也看到了赋值,但是并无订阅的打印

那么是否此框架出现了问题?

这时先使用一个常规方法来验证一下

textFiled.addTarget(self, action: #selector(textFiledChange), for: .allEditingEvents)

同样在点击屏幕后虽然还是给UITextField进行了赋值,但是textFiledChange并未响应,这里说明刚才的问题并不是出了bug,原因是因为赋值的操作并不是属于control event事件,如果只是赋值操作之后可以通过方法sendActionsForControlEvents手动的进行通知

所以如果对UITextField进行赋值但同时也想得到回调,则可以使用方法sendActionsForControlEvents,如下即可

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {

    print("12345")
    textFiled.text = "field-123456"
    textView.text += "-123"
    textFiled.sendActions(for: .allEditingEvents)
    
}

两次订阅(初始化订阅和点击控件的订阅)

在初始化的时候和点击控件的时候都会各有一次的订阅事件,那么现在来看看这两次的调用都是怎么来的

image.png

从上图可以看出textControlProperty类型,最终返回的是value,重点应该关注到controlPropertyWithDefaultEvents

image.png

image.png

最终调用到上图的函数中,这里会将最终创建的ControlProperty类型序列返回出去,接下来是外边对这个队列进行订阅subscribe来触发的

image.png

回调用会来到下图所示这里

image.png

第一次的发送就是这里observer.on(.next(getter(control))),此时textFieldtext为空,所以第一次会发送一个空值出去

同时往下看,其实会给textField添加一个target-action事件,只要触发allEditingEvents 或者valueChanged中的一个事件,便会调用eventHandler,在eventHandler中其实调用的就是创建ControlTarget时的尾随闭包

image.png

image.png

image.png

也就是说一旦点击了textField就会使其调用发送信号的命令。

以上即为UITextField在初始化和在点击控件时产生的两次订阅

UITextView

不同于UITextFieldUITextView在赋值的时候同样可以拿到订阅,因为其内部的实现是用通知来完成的

image.png

Driver

访问网络时可能发生的问题

func normalNetworkObservableUse() {

    let result = inputTF.rx.text.skip(1).flatMap { input in

        return self.dealWithData(inputText: input!)

    }

    _ = result.subscribe(onNext: { element in

        print("第一次订阅到:\(element) ----- \(Thread.current)")

    })

    _ = result.subscribe(onNext: { element in

        print("第二次订阅到:\(element) ----- \(Thread.current)")

    })

}

func dealWithData(inputText: String)-> Observable<Any> {

    print("请求网络:\(Thread.current)")

    return Observable<Any>.create({ ob -> Disposable in

        if inputText == "1234" {

            ob.onError(NSError.init(domain: "com.google.cn", code: 10086, userInfo: nil))

        }

        DispatchQueue.global().async {

            print("发送之前:\(Thread.current)")

            ob.onNext("已经输入:\(inputText)")

            ob.onCompleted()

        }

        return Disposables.create()

    })

}

上述代码是在模仿一个网络请求的场景,打印结果如下:

image.png

从中可以发现三个问题:

  • 每一次订阅都会访问一次网络,造成浪费流量
  • 回调回来后可能会造成子线程更新UI
  • 无法处理发送的错误事件

那么针对以上三个问题在此代码中要如何修改?

请求多次网络

在请求多次网络这个问题上可以如下修改,这样可以保证状态共享从而使得网络请求只有一次,哪怕是多次订阅

let result = inputTF.rx.text.skip(1).flatMap { input in
    return self.dealWithData(inputText: input!)
}.share(replay: 1, scope: .whileConnected)

回调的环境处于子线程

这样使得序列会包装源序列以便在指定的调度调度运行它的观察者回调,在此例中便是可以在主线程进行回调

let result = inputTF.rx.text.skip(1).flatMap { input in
    return self.dealWithData(inputText: input!).observe(on: MainScheduler.instance)
}.share(replay: 1, scope: .whileConnected)

错误问题处理

由于无法直接将错误返回给UI来进行处理,在使用此方法后可以将错误转化为字符串类型来处理

let result = inputTF.rx.text.skip(1).flatMap { input in
    return self.dealWithData(inputText: input!)
          .observe(on: MainScheduler.instance)
          .catchAndReturn("捕捉到错误")
}.share(replay: 1, scope: .whileConnected)

使用Driver

上面的代码完全可以用Driver来替换完成以上功能

let result = inputTF.rx.text.asDriver().flatMap { input in
        return self.dealWithData(inputText: input!).asDriver(onErrorJustReturn: "监测到错误事件")
    }
    
 _ = result.map { "长度:\(($0 as! String).count)" }.drive(self.textLabel.rx.text)
 _ = result.map { "\($0)" }.drive(self.btn.rx.title())

image.png

在上图的方法中可以看到observe(on:DriverSharingStrategy.scheduler)其实相当于之前例子中的observe(on: MainScheduler.instance),因为看到内部既是如此,所以这里相当于在主线程来调度的意思

image.png

image.png

image.png

catchAndReturn(onErrorJustReturn)就是之前的错误处理

而共享状态则是被封装在了内部

image.png

image.png