web-dev-qa-db-ja.com

RxSwiftでの双方向バインディング

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が変更されると、変数に通知して変数の値を設定し、変数の値が設定されている間はプロパティに通知します。無限のループにつながると思います...

10
leizh00701

質問をしてくれてありがとう、私は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<E>)がトリガーされ、スイッチ値が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がトリガーされると、コントロールからの値が発行されます
  • 値がコントロールプロパティに直接割り当てられている場合、コントロールは変更イベントをトリガーしないため、ループは発生しません。

それが役立つことを願っています、少し厄介な説明を申し訳ありません-私は実験によってそれを見つけました)

9
NikGreen

bindTo ????を使用できると思います。 ControlProperty <-> VariableおよびVariable <-> 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 <-> Variableの例(UITextFieldUITextViewなど)は RxSwiftPlayerプロジェクト にあります

// 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
15
Scott Gardner

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)
}
5
Anirudha Mahale

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)
1

@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
        )
    }

したがって、2つのイベントUIControl.Event = [.allEditingEvents, .valueChanged]だけを観察できます。

変数が変更され、変数がControlPropertyにバインドされ、ControlPropertyが[.allEditingEvents, .valueChanged]のために変更されなかったため、実行されました。

ControlPropertyが変更され、ControlPropertyが変数にバインドされ、変数が変更され、ControlPropertyにバインドされ、[.allEditingEvents, .valueChanged]が原因ではなく、ControlPropertyが設定されてから実行されます。


controlPropertyのソースコードで、UIControlターゲット-アクションを確立します。

[.allEditingEvents, .valueChanged]には、editingDidBegin、editingChanged、editingDidEnd、editingDidEndOnExit、valueChanged、

したがって、textField.textに直接割り当てると、イベントはトリガーされません。

1
black_pearl

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は変数への双方向バインディングです。

セッターメソッドのif判定のため、ControlPropertyは常に変更されるとは限りません。

RxSwift5.0.1にチェックインしました

0
dengApro