【问题标题】:Two way binding in RxSwiftRxSwift 中的两种方式绑定
【发布时间】:2016-09-26 12:53:05
【问题描述】:

我在 RxSwift 的示例代码中阅读了双向绑定运算符。

func <-> <T>(property: ControlProperty<T>, variable: Variable<T>) -> Disposable {
    let bindToUIDisposable = variable.asObservable()
        .bindTo(property)
    let bindToVariable = property
        .subscribe(onNext: { n in
            variable.value = n
        }, onCompleted:  {
            bindToUIDisposable.dispose()
        })

    return StableCompositeDisposable.create(bindToUIDisposable, bindToVariable)
}

当property改变时,它会通知变量,并设置变量的值,当变量的值被设置时,它会通知属性。我认为这会导致无限循环......

【问题讨论】:

    标签: ios swift functional-programming reactive-cocoa rx-swift


    【解决方案1】:

    我相信你可以使用bindTo?。以下是ControlProperty &lt;-&gt; Variable 和Variable &lt;-&gt; Variable 的实现:

    infix operator <-> { precedence 130 associativity left }
    
    func <-><T: Comparable>(property: ControlProperty<T>, variable: Variable<T>) -> Disposable {
        let variableToProperty = variable.asObservable()
            .distinctUntilChanged()
            .bindTo(property)
    
        let propertyToVariable = property
            .distinctUntilChanged()
            .bindTo(variable)
    
        return StableCompositeDisposable.create(variableToProperty, propertyToVariable)
    }
    
    func <-><T: Comparable>(left: Variable<T>, right: Variable<T>) -> Disposable {
        let leftToRight = left.asObservable()
            .distinctUntilChanged()
            .bindTo(right)
    
        let rightToLeft = right.asObservable()
            .distinctUntilChanged()
            .bindTo(left)
    
        return StableCompositeDisposable.create(leftToRight, rightToLeft)
    }
    

    ControlProperty &lt;-&gt; Variable 的示例(例如UITextField 和UITextView)在RxSwiftPlayer project 中

    // Example of Variable <-> Variable
    
    let disposeBag = DisposeBag()
    let var1 = Variable(1)
    let var2 = Variable(2)
    
    (var1 <-> var2).addDisposableTo(disposeBag)
    
    var1.value = 10
    print(var2.value) // 10
    
    var2.value = 20
    print(var1.value) // 20
    

    【讨论】:

    • 变量 变量的好例子。但是我不能使它与变量 等数组一起工作。有什么建议吗?
    【解决方案2】:

    感谢您提出这个问题,我花了一些时间研究ControlProperty 的实现(注意我添加了一个.debug() 调用来跟踪为控件属性生成的值)。

    public struct ControlProperty<PropertyType> : ControlPropertyType {
        public typealias E = PropertyType
    
        let _values: Observable<PropertyType>
        let _valueSink: AnyObserver<PropertyType>
    
        public init<V: ObservableType, S: ObserverType where E == V.E, E == S.E>(values: V, valueSink: S) {
            _values = values.debug("Control property values").subscribeOn(ConcurrentMainScheduler.instance)
            _valueSink = valueSink.asObserver()
        }
    
        public func on(event: Event<E>) {
            switch event {
            case .Error(let error):
                bindingErrorToInterface(error)
            case .Next:
                _valueSink.on(event)
            case .Completed:
                _valueSink.on(event)
            }
        }
    }
    

    我的测试设置如下,我已经删除了这里的所有视图定位以使其更短:

    import UIKit
    import RxSwift
    import RxCocoa
    class ViewController: UIViewController {
        let variable = Variable<Bool>(false);
        let bag = DisposeBag();
    
        override func loadView() {
            super.loadView()
    
            let aSwitch = UISwitch();
            view.addSubview(aSwitch)
    
            (aSwitch.rx_value <-> variable).addDisposableTo(bag);
    
            let button = UIButton();
            button.rx_tap.subscribeNext { [weak self] in
                self?.variable.value = true;
            }.addDisposableTo(bag)
            view.addSubview(button);
        }
     }
    
    infix operator <-> {
    }
    
    func <-> <T>(property: ControlProperty<T>, variable: Variable<T>) -> Disposable{
        let bindToUIDisposable = variable.asObservable().debug("Variable values in bind")
        .bindTo(property)
    
        let bindToVariable = property
            .debug("Property values in bind")
            .subscribe(onNext: { n in
                variable.value = n
                }, onCompleted:  {
                     bindToUIDisposable.dispose()
            })
    
        return StableCompositeDisposable.create(bindToUIDisposable, bindToVariable)
     }
    

    现在来看结果。首先我们尝试点击按钮,它应该将变量设置为true。这会在 ControlProperty 上触发 on(event: Event&lt;E&gt;) 并将开关值设置为 true。

    2016-05-28 12:24:33.229: Variable values in bind -> Event Next(true)
    
    // value flow
    value assigned to Variable -> 
    Variable emits event -> 
    ControlProperty receives event -> 
    value assigned to underlying control property (e.g. `on` for `UISwitch`)
    

    接下来让我们自己触发开关。如我们所见,控件生成一个事件作为UIControlEventValueChanged 的结果,该事件通过ControlProperty 上的_values 传递,然后其值被分配给Variable 值,如上例所示。但是没有循环,因为更新Variable 值不会触发开关上的控制事件。

    2016-05-28 12:29:01.957: Control property values -> Event Next(false)
    2016-05-28 12:29:01.957: Property values in bind -> Event Next(false)
    2016-05-28 12:29:01.958: Variable values in bind -> Event Next(false)
    
    // value flow
    trigger the state of control (e.g. `UISwitch`) -> 
    ControlProperty emits event -> 
    value assigned to Variable -> 
    Variable emits event -> 
    ControlProperty receives event -> 
    value assigned to underlying control property (e.g. `on` for `UISwitch`)
    

    所以一个简单的解释是:

    • 一旦触发某种UIControlEvent,就会发出来自控件的值
    • 当一个值直接分配给控件属性时,控件不会触发更改事件,因此没有循环。

    希望对您有所帮助,抱歉解释有点混乱——我是通过实验发现的)

    【讨论】:

      【解决方案3】:

      您输入的任何内容都会在 5 秒后清晰显示。这是从上面拍摄的answer

      import UIKit
      import RxSwift
      import RxCocoa
      
      class UserViewModel {
          let username = BehaviorSubject<String?>(value: "")
      }
      
      class ViewController: UIViewController {
      
          @IBOutlet weak var email: UITextField!
      
          var userViewModel = UserViewModel()
          let bag = DisposeBag()
      
          override func viewDidLoad() {
              super.viewDidLoad()
      
              userViewModel.username.asObservable().subscribe { print($0) }.disposed(by: bag)
              (email.rx.text <-> userViewModel.username).disposed(by: bag)
      
              // clear value of the username.
              DispatchQueue.main.asyncAfter(deadline: .now()+5) {
                  self.userViewModel.username.onNext(nil)
              }
          }
      }
      
      infix operator <->
      
      @discardableResult func <-><T>(property: ControlProperty<T>, variable: BehaviorSubject<T>) -> Disposable {
          let variableToProperty = variable.asObservable()
              .bind(to: property)
      
          let propertyToVariable = property
              .subscribe(
                  onNext: { variable.onNext($0) },
                  onCompleted: { variableToProperty.dispose() }
          )
      
          return Disposables.create(variableToProperty, propertyToVariable)
      }
      

      【讨论】:

      • 这很酷!想知道为什么这不是 RxSwift/RxCocoa 本身的一部分。
      • 不错。我会考虑删除@discardableResult,因为这样编译器会警告你,以防你忘记将一次性物品装箱。
      【解决方案4】:

      将BehaviourRelay 绑定回控件属性没有障碍。您只需要过滤具有相同值的事件(以防止无限循环)。

      例如,就我而言,我需要将电子邮件绑定到文本字段。但我想在电子邮件输入期间删除空格。 这是我如何实现它的示例:

      emailTextField.rx.text
        .map { $0?.trimmingCharacters(in: CharacterSet.whitespaces) } // remove whitespaces from input
        .bind(to: viewModel.email)
        .disposed(by: disposeBag)
      
      // Just filter all events with no actual value change to prevent infinite loop
      viewModel.email
        .filter { $0 != self.emailTextField.text } // if it removed whitespaces in mapping, values will not match and text in text field will be updated
        .bind(to: emailTextField.rx.text)
        .disposed(by: disposeBag)
      

      【讨论】:

        【解决方案5】:

        UITextField+Rx.swift中的源码:

        /// Reactive wrapper for `text` property.
            public var value: ControlProperty<String?> {
                return base.rx.controlPropertyWithDefaultEvents(
                    getter: { textField in
                        textField.text
                    },
                    setter: { textField, value in
                        // This check is important because setting text value always clears control state
                        // including marked text selection which is imporant for proper input 
                        // when IME input method is used.
                        if textField.text != value {
                            textField.text = value
                        }
        
                    }
                )
            }
        

        神奇之处在于二传手:

        if textField.text != value {
             textField.text = value
        }
        

        所以 ControlProperty 是绑定到变量的两种方式,

        ControlProperty不会一直改变,因为setter方法中的if判断。

        我签入了 RxSwift 5.0.1

        【讨论】:

          【解决方案6】:

          @dengApro 的回答非常接近。

          UITextField+Rx.swift中的源码:

           /// Reactive wrapper for `text` property.
              public var value: ControlProperty<String?> {
                  return base.rx.controlPropertyWithDefaultEvents(
                      getter: { textField in
                          textField.text
                      },
                      setter: { textField, value in
                          // This check is important because setting text value always clears control state
                          // including marked text selection which is imporant for proper input 
                          // when IME input method is used.
                          if textField.text != value {
                              textField.text = value
                          }
                      }
                  )
              }
          

          分配textField 一个值不能被订阅,因为controlPropertyWithDefaultEvents

          UIControl+Rx.swift中的源码:

          /// This is a separate method to better communicate to public consumers that
              /// an `editingEvent` needs to fire for control property to be updated.
              internal func controlPropertyWithDefaultEvents<T>(
                  editingEvents: UIControl.Event = [.allEditingEvents, .valueChanged],
                  getter: @escaping (Base) -> T,
                  setter: @escaping (Base, T) -> Void
                  ) -> ControlProperty<T> {
                  return controlProperty(
                      editingEvents: editingEvents,
                      getter: getter,
                      setter: setter
                  )
              }
          

          所以只有UIControl.Event = [.allEditingEvents, .valueChanged]这两个事件是可以观察到的,

          变量改变,变量绑定到 ControlProperty,ControlProperty 改变不是因为[.allEditingEvents, .valueChanged],然后完成。

          ControlProperty 改变,ControlProperty 绑定到Variable,Variable 改变并绑定到ControlProperty,ControlProperty 设置不是因为[.allEditingEvents, .valueChanged],然后完成。


          在controlProperty的源代码中,会建立UIControl目标--action。

          [.allEditingEvents, .valueChanged] 包含editingDidBegin、editingChanged、editingDidEnd、editingDidEndOnExit、valueChanged,

          所以直接分配给textField.text不会触发任何事件。

          【讨论】:

            猜你喜欢
            • 2017-09-05
            • 1970-01-01
            • 2021-10-08
            • 2016-02-27
            • 1970-01-01
            • 1970-01-01
            • 2014-02-13
            • 1970-01-01
            • 2012-01-14
            相关资源
            最近更新 更多