RxSwift:GUIでの作業





RxSwiftについての私の最初の記事は、ほとんどすべての基本的な演算子を網羅していましたが、開発に飛び込むことはあまり意味がありませんでした。 しかし、これは関数型プログラミングの単なるアルファベットです。 本格的なプログラムを作成するには、GUIを使用する際の基本原則を理解する必要があります。



ほとんどの場合、RxExampleの標準的な例が資料の学習に使用されますが、特定のポイントを明確にするために、UIExplanationサンドボックスとRxExampleの追加の例が作成されました。

すべてのコードはgithub.com/sparklone/RxSwiftにあります



RxでUI要素を使用する場合、基本的なニーズがあります。

1) 原則として私たちを待っている落とし穴と、なぜドライバーが必要なのかを理解する

2) Observable要素が要素のUIのプロパティ/プロパティの状態を変更するように、UIをObservableにバインドする方法を学びます。 これはUIBindingObserverを使用して解決されます

3) Rxレール上のターゲットアクションパターンを変換する方法を学びます。 これはControlEventを使用して行われます。

4) UI要素のプロパティへの双方向バインディングを作成します。 これは、ControlPropertyを使用して行われます。

5) 以来 多くの場合、UI要素のデリゲート/データソースは単数形で想定されます-DelegateProxyクラスが導入されました。これにより、通常のデリゲートとRxシーケンスの両方を同時に使用できます。



各ニーズを個別に検討します。







運転手





1)Observableを使用する場合、いくつかの問題があります。 それらを理解するために、サンドボックスの小さなを検討してください



import Cocoa import RxSwift import RxCocoa import XCPlayground XCPlaygroundPage.currentPage.needsIndefiniteExecution = true example("without shareReplay duplicate call problem") { let source = NSTextField() let status = NSTextField() let URL = NSURL(string: "https://github.com/")! let request = NSURLRequest(URL: URL) let observable = NSURLSession.sharedSession().rx_response(request).debug("http") let sourceObservable = observable.map { (maybeData, response) in return String(data: maybeData, encoding: NSUTF8StringEncoding)! }.observeOn(MainScheduler.instance) let statusObservable = observable.map { (maybeData, response) in return response.statusCode.description }.observeOn(MainScheduler.instance) sourceObservable.subscribe(source.rx_text) statusObservable.subscribe(status.rx_text) }
      
      







a)Observableがあり、複数のObserverに署名する場合-Observerごとに個別のObservableが作成されます。 この場合、Observableはネットワークに接続してページをダウンロードし、ページコードとサーバーの応答ステータスは異なるtextViewに配置されます。



コンソールを見ると、サブスクライブされた2つ、破棄された2つが表示されます。

 --- without shareReplay duplicate call problem example --- 2016-05-01 04:17:17.225: http -> subscribed 2016-05-01 04:17:17.229: http -> subscribed curl -X GET "https://github.com/" -i -v Success (1098ms): Status 200 2016-05-01 04:17:18.326: http -> Event Next((<OS_dispatch_d...; mode=block"; } })) 2016-05-01 04:17:18.339: http -> Event Completed 2016-05-01 04:17:18.339: http -> disposed curl -X GET "https://github.com/" -i -v Success (1326ms): Status 200 2016-05-01 04:17:18.556: http -> Event Next((<OS_dispatch_d...; mode=block"; } })) 2016-05-01 04:17:18.557: http -> Event Completed 2016-05-01 04:17:18.557: http -> disposed
      
      







これを回避するには、元のオブザーバブルにshareReplayLatestWhileConnectedを追加します

 let observable = NSURLSession.sharedSession().rx_response(request).debug("http").shareReplayLatestWhileConnected()
      
      







その結果、コンソールには、サーバーへの呼び出しが1つしかないことが示されます。

 --- with shareReplay no duplicate call problem example --- 2016-05-01 04:18:27.845: http -> subscribed curl -X GET "https://github.com/" -i -v Success (960ms): Status 200 2016-05-01 04:18:28.807: http -> Event Next((<OS_dispatch_d...; mode=block"; } })) 2016-05-01 04:18:28.820: http -> Event Completed 2016-05-01 04:18:28.821: http -> disposed
      
      







また、shareReplay(1)ではなくshareReplayLatestWhileConnectedが使用されていることに注意してください。 すべてのオブザーバーのサブスクライブを解除したとき、およびシーケンスが正しく完了したとき、またはエラーが発生したときに、バッファーをクリアします。 RxSwiftオペレーターに関する最初の記事を書いたとき、私はサンドボックスで自分でshareReplayのこの奇妙な動作(シーケンスが完了した後でもクリーニングの欠如)を発見し、最初に何かが間違っていると判断しました-設計により。



b)MainSchedulerのGUIに接続されているすべてを処理する必要があります。 異なるスケジューラに関するメモリを更新する必要がある場合。 公式ドキュメントを参照し、前の記事でsubscribeOnobserveOnを説明したリンクをたどることができます。

コードから.observeOn(MainScheduler.instance)を削除すると、次のようになります

 fatalError "fatal error: Executing on backgound thread. Please use `MainScheduler.instance.schedule` to schedule work on main thread."
      
      





ところで、私はこのエラーに多少困惑しました。Observableを作成するスレッドで、その中のコードがその中で実行されることを知っていたからです。 しかし、サブスクライブ呼び出しがどのスレッドで行われているのか、これでオブザーバーコードが実行されると誤って信じていました。



最初の瞬間、Observableコードは実際に作成されたのと同じスレッドで実行されます。 しかし、rx_responseの場合、内部にObservableが作成され、その中にNSURLSessionからdataTaskWithRequestメソッドへの呼び出しがあり、値の戻りはこのメソッドのクロージャーから来ており、このクロージャーは完全に異なるスレッドですでに実行されています。 したがって、NSURLSession.sharedSession()。Rx_response(request)の出口で、別のスレッドが待機します。



そして2番目の瞬間-公式ドキュメントを読んだ後、私はあなたがサブスクライブを呼び出しているスレッドを誤って考えました-オブザーバーの本体はこのスレッドで実行されますが、そうではないことが判明しました。

これを確認するために、さらに2つの例を作成しました



 example("from main thread") { print("init thread: \(NSThread.currentThread())") let source = NSTextField() let status = NSTextField() let URL = NSURL(string: "https://github.com/")! let request = NSURLRequest(URL: URL) let observable = NSURLSession.sharedSession().rx_response(request).shareReplayLatestWhileConnected() let sourceObservable = observable.map { (maybeData, response) in return String(data: maybeData, encoding: NSUTF8StringEncoding)! } sourceObservable.subscribe() { e in print("observer thread: \(NSThread.currentThread())") } } example("from another queue") { print("init thread: \(NSThread.currentThread())") let source = NSTextField() let status = NSTextField() let URL = NSURL(string: "https://github.com/")! let request = NSURLRequest(URL: URL) let observable = NSURLSession.sharedSession().rx_response(request).shareReplayLatestWhileConnected() let sourceObservable = observable.map { (maybeData, response) in return String(data: maybeData, encoding: NSUTF8StringEncoding)! } let queue1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) dispatch_async(queue1,{ print("queue1 thread: \(NSThread.currentThread())") sourceObservable.subscribe() { e in print("observer thread: \(NSThread.currentThread())") } }) }
      
      







コンソール出力:



 --- from main thread example --- init thread: <NSThread: 0x7fc298d12ec0>{number = 1, name = main} curl -X GET "https://github.com/" -i -v Success (944ms): Status 200 observer thread: <NSThread: 0x7fc298fbf1a0>{number = 3, name = (null)} observer thread: <NSThread: 0x7fc298fbf1a0>{number = 3, name = (null)} --- from another queue example --- init thread: <NSThread: 0x7ff182d12ef0>{number = 1, name = main} queue1 thread: <NSThread: 0x7ff182d5c3e0>{number = 3, name = (null)} curl -X GET "https://github.com/" -i -v Success (956ms): Status 200 observer thread: <NSThread: 0x7ff185025950>{number = 4, name = (null)} observer thread: <NSThread: 0x7ff185025950>{number = 4, name = (null)}
      
      







どちらの例でも、observeOnは使用していません。 どちらの場合でもわかるように、オブザーバー内のコードは、サブスクライブしたコードのストリームでは実行されませんが、rx_responseから返されたという事実(これは、RxプロジェクトからNSURLSession + Rxファイル内のフローを記録することで確認できます)



 public func rx_response(request: NSURLRequest) -> Observable<(NSData, NSHTTPURLResponse)> { return Observable.create { observer in print("RXRESPONSE thread: \(NSThread.currentThread())") ...... let task = self.dataTaskWithRequest(request) { (data, response, error) in print("TASK thread: \(NSThread.currentThread())")
      
      







c)Observableコードの処理中にエラーが発生した場合、デバッグモードでfatalErrorをキャッチし、Release-エラー「UIにエラーをバインド:引数が範囲外です」がコンソールに移動し、UIに関連するすべてをこのObservableに自動的にバインド解除します。



これがどのように発生するかを確認するために、元のIntroductionExampleViewControllerをわずかに変更しました。 私はdisposeButton.rx_tapへのバインディングをコメントアウトし、代わりに私のものを作成しました(githubでは、実装を即座に変更できるようにバージョンをコメントアウトしました)



 disposeButton.rx_tap.debug("rx_tap") .flatMap{ value in return Observable<String>.create{ observer in observer.on(.Next("1")) observer.onError(RxError.ArgumentOutOfRange) return NopDisposable.instance } } .bindTo(a.rx_text) .addDisposableTo(disposeBag)
      
      







リリースモードでは、起動時にコンソールが表示されます

 2016-04-30 02:02:41.486: rx_tap -> subscribed
      
      







そして、最初にボタンを押したとき



 2016-04-30 02:02:48.248: rx_tap -> Event Next(()) Binding error to UI: Argument out of range. 2016-04-30 02:02:48.248: rx_tap -> disposed
      
      







さらにボタンを押しても何も起こりません。 rx_tapは破棄されました



その結果、これらのポイントに従わず、ドライバーが作成されたため、次の3つだけが保証されます。

a)データはshareReplayLatestWhileConnectedを使用して共有されます

b)スレッドはMainSchedulerで実行されます(大まかにUIスレッド)

c)エラーは生成されません(エラーの代わりに返す値を決定します)



したがって、ドライバーの作成は、公式ドキュメントで行われているように表すことができます。



 let safeSequence = xs .observeOn(MainScheduler.instance) // observe events on main scheduler .catchErrorJustReturn(onErrorJustReturn) // can't error out .shareReplayLatestWhileConnected // side effects sharing return Driver(raw: safeSequence) // wrap it up
      
      







subscribe()の代わりにdrive()を見ると、uiで安全に作業できることがわかります。



ここで、GitHubSignupの例を見てみましょう。ドライバーを使用して、ドライバーを使用して、または使用せずにコードを比較するだけです。

Driverを使用しない場合、viewModel作成コードは次のようになります。



 let viewModel = GithubSignupViewModel1( input: ( username: usernameOutlet.rx_text.asObservable(), password: passwordOutlet.rx_text.asObservable(), repeatedPassword: repeatedPasswordOutlet.rx_text.asObservable(), loginTaps: signupOutlet.rx_tap.asObservable() ) ...
      
      





なぜなら rx_text-ControlProperty、次にasObservableは変換せずに内部Observableを返します



今、それはドライバーでどうですか



 let viewModel = GithubSignupViewModel2( input: ( username: usernameOutlet.rx_text.asDriver(), password: passwordOutlet.rx_text.asDriver(), repeatedPassword: repeatedPasswordOutlet.rx_text.asDriver(), loginTaps: signupOutlet.rx_tap.asDriver() ), ...
      
      





違いは、asObservable-asDriverではなく、上記の3つの条件の充足につながります。



アプリケーションを使用する場合、ドライバーのサブスクライブ/バインドがなく、その変更が使用されるので、違いも最小限です



 viewModel.signupEnabled .subscribeNext { [weak self] valid in self?.signupOutlet.enabled = valid self?.signupOutlet.alpha = valid ? 1.0 : 0.5 } .addDisposableTo(disposeBag) viewModel.validatedUsername .bindTo(usernameValidationOutlet.ex_validationResult) .addDisposableTo(disposeBag)
      
      







ドライバーでは、ドライブとその修正を使用します



 viewModel.signupEnabled .driveNext { [weak self] valid in self?.signupOutlet.enabled = valid self?.signupOutlet.alpha = valid ? 1.0 : 0.5 } .addDisposableTo(disposeBag) viewModel.validatedUsername .drive(usernameValidationOutlet.ex_validationResult) .addDisposableTo(disposeBag)
      
      





ドライバーが作成されるGithubSignupViewModel1 / GithubSignupViewModel2を見ると、もう少し面白いです



GithubSignupViewModel1の冗長コード



 validatedUsername = input.username .flatMapLatest { username in return validationService.validateUsername(username) .observeOn(MainScheduler.instance) .catchErrorJustReturn(.Failed(message: "Error contacting server")) } .shareReplay(1)
      
      







に簡略化



 validatedUsername = input.username .flatMapLatest { username in return validationService.validateUsername(username) .asDriver(onErrorJustReturn: .Failed(message: "Error contacting server")) }
      
      







この知識の適切なアプリケーションは、UIを操作する際の基本的なエラーからすでに保護されているはずです。 しかし、これでも十分ではありません。必要に応じて拡張機能を作成するために、Rxを操作するための標準UI要素がどのように展開されるかを理解する必要があります。







UIBindingObserver



2)GeolocationViewController.swiftを例として使用すると、UI要素に独自のオブザーバーを掛ける方法を確認できます。



 private extension UILabel { var rx_driveCoordinates: AnyObserver<CLLocationCoordinate2D> { return UIBindingObserver(UIElement: self) { label, location in label.text = "Lat: \(location.latitude)\nLon: \(location.longitude)" }.asObserver() } }
      
      







したがって、UIBindingObserverは、クロージャー(この場合は場所)に渡されたパラメーターを、渡されたオブジェクト(この場合はtextプロパティ)のプロパティ/プロパティの変更にバインドできる汎用ヘルパークラスです。 UIBindingObserverは、オブジェクトのクラス(この場合はUILabel、拡張UILabel)によってパラメーター化され、オブジェクト(ラベル)とオブジェクトの状態を変更する値(場所)の両方がパラメーターとして渡されます

この例の場所パラメーターのタイプは、戻り値AnyObserver <CLLocationCoordinate2D>のパラメーター化により自動的に決定されます

たとえば、このコードは機能しません



 var rx_driveCoordinates: AnyObserver<CLLocationCoordinate2D> { let observer = UIBindingObserver(UIElement: self) { label, location in label.text = "Lat: \(location.latitude)\nLon: \(location.longitude)" } return observer.asObserver() }
      
      





実際、オブザーバーの作成時には、UIBindingObserverはロケーションがどのタイプになるかわかりません。なぜなら、オリジナルとは異なり、クロージャーからすぐには戻らないからです。 自動検出タイプの「魔法」は機能しません。

しかし、これはすでに機能します。なぜなら UIBindingObserverの作成時に、すべてのパラメーターのタイプを明示的に指定しました



 var rx_driveCoordinates: AnyObserver<CLLocationCoordinate2D> { let uiBindingObserver: UIBindingObserver<UILabel, CLLocationCoordinate2D> = UIBindingObserver(UIElement: self) { label, location in label.text = "Lat: \(location.latitude)\nLon: \(location.longitude)" } return uiBindingObserver.asObserver() }
      
      







まとめると。 一方で、この発言はRxSwiftに直接関連するものではなく、Swiftが転送された値のタイプとその自動認識をどのように処理するかという参照であり、ルーチンの明示的なタイプ指定から私たちを救います。 一方、RXSwiftバインダーには魔法がないことを理解することが重要です。 それがどこから来て転送されるのかを知っていれば、留めるためのタスクを思いつくことができます-例えば、クロージャーに渡されるBool型のパラメーターの値に応じてUILabelのテキストの色を変えたいです。 trueの場合、テキストの色が赤になり、falseの場合は黒になります

必要なのは、オブザーバが定義されたときに返されるBoolをパラメータ化し、クロージャ内でこの知識を正しく使用することです



 var rx_wasError: AnyObserver<Bool> { return UIBindingObserver(UIElement: self) { label, error in label.textColor = error ? UIColor.redColor() : UIColor.blackColor() }.asObserver() }
      
      







さて、最後のポイントは、UIBindingObserverを返さないのはなぜですか、なぜAnyObserverにキャストするのですか? そうでなければ、戻り値の型をオブジェクト型(UILabel)でパラメーター化する必要があります。これは、タスクのフレームワークでは絶対に重要ではありません。



 var rx_driveCoordinatesUIB: UIBindingObserver<UILabel, CLLocationCoordinate2D> { return UIBindingObserver(UIElement: self) { label, location in label.text = "Lat: \(location.latitude)\nLon: \(location.longitude)" } }
      
      







私たちは正しいですか? AnyObserverの定義を調べます



/ **

型消去された `ObserverType`。



同じ「Element」型を持つ任意の基になるオブザーバー型に操作を転送し、基になるオブザーバー型の詳細を隠します。

* /

実際、AnyObserverは渡されたオブジェクトの型を隠すラッパーであり、クロージャーに渡されたパラメーターの型のみを残します。



次の拡張機能は、得られた知識のおかげで読みやすくなります。 Bool型のクロージャーに渡されるパラメーターに応じて、UIViewを非表示にするか、またはその逆で表示します。



 private extension UIView { var rx_driveAuthorization: AnyObserver<Bool> { return UIBindingObserver(UIElement: self) { view, authorized in if authorized { view.hidden = true view.superview?.sendSubviewToBack(view) } else { view.hidden = false view.superview?.bringSubviewToFront(view) } }.asObserver() } }
      
      









コントロールベント





3)Rx環境でターゲットイベントパターンを制御するために、ControlEvent構造<>を導入しました

次のプロパティがあります。

-彼女のコードは決して落ちません

-サブスクリプション時に初期値は送信されません

-メモリが解放されると、コントロールは.Completedを生成します

-エラーは発生しません

-すべてのイベントはMainSchedulerで実行されます



簡単なボタンを押す例を考えてみましょう。 rx_tapプロパティが定義されているUIButtonの拡張機能が作成されました

 extension UIButton { /** Reactive wrapper for `TouchUpInside` control event. */ public var rx_tap: ControlEvent<Void> { return rx_controlEvent(.TouchUpInside) } }
      
      







UIControlの場合、拡張機能はメソッドを定義します



 public func rx_controlEvent(controlEvents: UIControlEvents) -> ControlEvent<Void> { let source: Observable<Void> = Observable.create { [weak self] observer in MainScheduler.ensureExecutingOnScheduler() //     Main  guard let control = self else { //      -  .Competed observer.on(.Completed) return NopDisposable.instance } //  ,  ControlTarget    ,            callback       let controlTarget = ControlTarget(control: control, controlEvents: controlEvents) { control in observer.on(.Next()) } return AnonymousDisposable { controlTarget.dispose() } }.takeUntil(rx_deallocated) //       return ControlEvent(events: source) }
      
      







ControlTargetクラス内では、イベントはすでにサブスクライブされています。

control.addTarget(self、action:selector、forControlEvents:controlEvents)



このような拡張機能の使用は、通常のObservableの使用と同じくらい簡単です

GeolocationExampleの例を検討するか、GeolocationViewControllerクラスを検討してください



 class GeolocationViewController: ViewController { @IBOutlet weak private var button: UIButton! ... override func viewDidLoad() { ... button.rx_tap .bindNext { [weak self] in self?.openAppPreferences() } .addDisposableTo(disposeBag) ... } ... }
      
      







ここでは、ボタンをクリックするたびにbindNextを作成し、クロージャーコードで設定パネルを開きます。

ちなみに、bindNextは、メインスレッドにいることを確認するサブスクライブの単なるラッパーです。



 public func bindNext(onNext: E -> Void) -> Disposable { return subscribe(onNext: onNext, onError: { error in let error = "Binding error: \(error)" #if DEBUG rxFatalError(error) #else print(error) #endif }) }
      
      







また、必要に応じて、ControlEvent-.asObservable()を使用したObservableまたは.asDriver()を使用したDriverからいつでも取得できます。





制御特性





4)UI要素のプロパティへの双方向のバインドを作成するために、ControlProperty <>構造は次のプロパティを使用して解決します。



-彼女のコードは決して落ちません

-要素のシーケンスにshareReplay(1)が適用されます

-メモリによって制御される場合、.Completedが生成されます

-エラーは発生しません

-すべてのイベントはMainSchedulerで実行されます



たとえば、もちろん、UITextFieldからtextプロパティを取得します



 extension UITextField { /** Reactive wrapper for `text` property. */ public var rx_text: ControlProperty<String> { return UIControl.rx_value( self, getter: { textField in textField.text ?? "" }, setter: { textField, value in textField.text = value } ) } }
      
      







rx_valueメソッドが何であるか見てみましょう



 static func rx_value<C: AnyObject, T: Equatable>(control: C, getter: (C) -> T, setter: (C, T) -> Void) -> ControlProperty<T> { let source: Observable<T> = Observable.create { [weak weakControl = control] observer in guard let control = weakControl else { //      -  .Competed observer.on(.Completed) return NopDisposable.instance } observer.on(.Next(getter(control))) //              getter' // ,     ControlTarget let controlTarget = ControlTarget(control: control as! UIControl, controlEvents: [.AllEditingEvents, .ValueChanged]) { _ in if let control = weakControl { observer.on(.Next(getter(control))) } } return AnonymousDisposable { controlTarget.dispose() } } .distinctUntilChanged() //       .takeUntil((control as! NSObject).rx_deallocated) //       //   ,   UIBindingObserver     Observable     setter let bindingObserver = UIBindingObserver(UIElement: control, binding: setter) return ControlProperty<T>(values: source, valueSink: bindingObserver) } }
      
      







ご覧のとおり、双方向バインディングは、すでに考慮されているControlTargetとUIBindingObserverの組み合わせです。

ControlPropertyの定義を見ると、ControlPropertyTypeプロトコルが実装されており、ObservableTypeとObserverTypeの両方を継承していることがわかります。

IntroductionExampleViewControllerコードをもう一度見てください



 @IBOutlet var a: NSTextField! @IBOutlet var b: NSTextField! @IBOutlet var c: NSTextField! ... override func viewDidLoad() { ... //  ControlProperty      Observable let sum = Observable.combineLatest(a.rx_text, b.rx_text) { (a: String, b: String) -> (Int, Int) in return (Int(a) ?? 0, Int(b) ?? 0) } ... sum .map { (a, b) in return "\(a + b)" } .bindTo(c.rx_text) //    Observer' .addDisposableTo(disposeBag) }
      
      







両方の動作が同時に必要な場合、つまり 双方向バインディングを行う-Rxコードで独自の演算子を作成する方法を見ることができます



 infix operator <-> { } 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) }
      
      







演算子を使用すると、バインディングを簡単かつ明確に作成できます。



 let textViewValue = Variable("") textView.rx_text <-> textViewValue
      
      









DelegateProxy





5)Cocoaアーキテクチャの基礎-デリゲート。 しかし、通常、オブジェクトごとに1つのデリゲートがあると想定されているため、RxにDelegateProxyクラスが追加されました。これにより、通常のデリゲートとRxシーケンスの両方を同時に使用できます。



ユーザーの観点から見ると、既存のAPIはそれほど複雑ではありません。

UISearchBarを例にとると、[キャンセル]ボタンをクリックしたときに何らかの形で応答したいとします。 私たちにとって、UISearchBarクラスの拡張では、変数が作成されます



 public var rx_cancelButtonClicked: ControlEvent<Void> { let source: Observable<Void> = rx_delegate.observe(#selector(UISearchBarDelegate.searchBarCancelButtonClicked(_:))) .map { _ in return () } return ControlEvent(events: source) }
      
      







彼女との作業は簡単でシンプルです。

 searchBar.rx_cancelButtonClicked.subscribeNext { _ in //    }
      
      







しかし、tableViewでの作業は少し気分を害しました。

例(SimpleTableViewExample)を使用すると、すべてが単純になります。



 class SimpleTableViewExampleViewController : ViewController { @IBOutlet weak var tableView: UITableView! override func viewDidLoad() { super.viewDidLoad() //  Observable   let items = Observable.just([ "First Item", "Second Item", "Third Item" ]) //     tableView (    dataSource),        items .bindTo(tableView.rx_itemsWithCellIdentifier("Cell", cellType: UITableViewCell.self)) { (row, element, cell) in cell.textLabel?.text = "\(element) @ row \(row)" } .addDisposableTo(disposeBag) //      , rx_modelSelected -   tableView:didSelectRowAtIndexPath: tableView .rx_modelSelected(String) .subscribeNext { value in DefaultWireframe.presentAlert("Tapped `\(value)`") } .addDisposableTo(disposeBag) //        -   tableView(_:accessoryButtonTappedForRowWithIndexPath:) tableView .rx_itemAccessoryButtonTapped .subscribeNext { indexPath in DefaultWireframe.presentAlert("Tapped Detail @ \(indexPath.section),\(indexPath.row)") } .addDisposableTo(disposeBag) } }
      
      







クールです。rx_itemsWithCellIdentifierはRx自体で定義されているため、誰でもアクセスできます。 わかった セクションのあるテーブルについてはどうですか? SimpleTableViewExampleSectionedの例を見てみましょう



 class SimpleTableViewExampleSectionedViewController : ViewController , UITableViewDelegate { @IBOutlet weak var tableView: UITableView! let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, Double>>() override func viewDidLoad() { super.viewDidLoad() let dataSource = self.dataSource let items = Observable.just([ SectionModel(model: "First section", items: [ 1.0, 2.0, 3.0 ]), SectionModel(model: "Second section", items: [ 1.0, 2.0, 3.0 ]), SectionModel(model: "Second section", items: [ 1.0, 2.0, 3.0 ]) ]) dataSource.configureCell = { (_, tv, indexPath, element) in let cell = tv.dequeueReusableCellWithIdentifier("Cell")! cell.textLabel?.text = "\(element) @ row \(indexPath.row)" return cell } items .bindTo(tableView.rx_itemsWithDataSource(dataSource)) .addDisposableTo(disposeBag) tableView .rx_itemSelected .map { indexPath in return (indexPath, dataSource.itemAtIndexPath(indexPath)) } .subscribeNext { indexPath, model in DefaultWireframe.presentAlert("Tapped `\(model)` @ \(indexPath)") } .addDisposableTo(disposeBag) tableView .rx_setDelegate(self) .addDisposableTo(disposeBag) } func tableView(tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let label = UILabel(frame: CGRect.zero) label.text = dataSource.sectionAtIndex(section).model ?? "" return label } }
      
      







RxTableViewSectionedReloadDataSourceに注意を払いますが、どこで定義されていますか? RxExampleプロジェクトでは、つまり 私が理解しているように、これはすべての人に推奨されている慣習的な解決策ではありませんが、たとえば。 内部を見ると、その理由が理解できます。くしゃみをするたびに、テーブル全体のデータをリロードすることをお勧めします。



 public func tableView(tableView: UITableView, observedEvent: Event<Element>) { UIBindingObserver(UIElement: self) { dataSource, element in dataSource.setSections(element) tableView.reloadData() }.on(observedEvent) }
      
      





控えめに言っても、最善の解決策ではありません。 代替手段は何ですか?繰り返しますが、RxTableViewSectionedAnimatedDataSourceクラスはRxExampleで定義されています。このdataSourceの使用例については、TableViewPartialUpdatesの例が提供されています。これと比較して、完全なデータ再読み込み(RxTableViewSectionedReloadDataSource)と部分(RxTableViewSectionedAnimatedDataSource)の両方を備えたパーティションを使用して、テーブル内のデータを更新する方法を示します。 CollectionViewでの作業の例を次に示します。しかし、編集の可能性を考慮せずにこれらすべてを行います。

さて、私はカードを手にしています。セクションと編集機能を備えたテーブルで作業する簡単な例を作成します。 TableViewEditPartialUpdateの例RxExampleの残りの例に置きます。



これがRxSwiftでの「戦闘」GUIコードの初めての経験であったため、レーキの一部をすぐに受け取りました。



 class TablViewControllerEditPartialUpdate : ViewController { @IBOutlet weak var tableView: UITableView! var sections = Variable([NumberSection]()) override func viewDidLoad() { super.viewDidLoad() // NumberSection -    typealias AnimatableSectionModel<String, Int> let items = [ NumberSection(model: "Section 1", items: [1, 3, 5]), NumberSection(model: "Section 2", items: [2, 4, 6, 8]), NumberSection(model: "Section 3", items: [7, 11, 10]) ] self.sections.value = items let editableDataSource = RxTableViewSectionedAnimatedDataSource<NumberSection>() configDataSource(editableDataSource) //    rx_itemsAnimatedWithDataSource,   rx_itemsWithDataSource self.sections.asObservable() .bindTo(tableView.rx_itemsAnimatedWithDataSource(editableDataSource)) .addDisposableTo(disposeBag) //     tableView.rx_itemDeleted.subscribeNext{[weak self] item in if let controller = self { controller.sections.value[item.section].items.removeAtIndex(item.row) } }.addDisposableTo(disposeBag) //        tableView .rx_modelSelected(IdentifiableValue<Int>) .subscribeNext { i in DefaultWireframe.presentAlert("Tapped `\(i)`") } .addDisposableTo(disposeBag) //  NSIndexPath     ,        tableView .rx_itemSelected .subscribeNext { [weak self] i in if let controller = self { print("Tapped `\(i)` - \(controller.sections.value[i.section].items[i.row].dynamicType)") } } .addDisposableTo(disposeBag) } func configDataSource(dataSource: RxTableViewSectionedDataSource<NumberSection>) { dataSource.configureCell = { (_, tv, ip, i) in let cell = tv.dequeueReusableCellWithIdentifier("Cell") ?? UITableViewCell(style:.Default, reuseIdentifier: "Cell") cell.textLabel!.text = "\(i)" return cell } dataSource.titleForHeaderInSection = { (ds, section: Int) -> String in return dataSource.sectionAtIndex(section).model } dataSource.canEditRowAtIndexPath = { (ds, ip) in return true } } }
      
      







1)このコードを記述し、ストーリーボードで作成されたTableViewControllerにクラスを登録し、実行しようとしました。 エラー。

 fatal error: Failure converting from <RxExample_iOS.TableViewControllerEditPartialUpdate: 0x7f895a643dd0> to UITableViewDataSource: file /Users/SparkLone/projects/repos/RxSwift/RxCocoa/Common/RxCocoa.swift, line 340
      
      





わあすぐにではなく、私は何が起こっているのかを理解しました。多くのRxコードは、荒野を抜けようとする無益な試みで突かれました。そして、デザイナでViewTableControllerを作成するときにデフォルトで、彼はコントローラをdataSourceとして示します。また、Rxはプロキシを作成するときに、現在のdataSourceをforwardToDelegateとして示しようとします。そして、私のコントローラーは標準形式でDataSourceを実装していません。もちろん、責任を負う人はいませんが、明らかにこの種のライブラリで動作し始めているので、あなたは無意識のうちにいくつかの厄介なバグを期待しています。



2)さて、彼らは厄介なバグを望んでいた-お願いします。

最初は文字列の代わりに



 rx_modelSelected(IdentifiableValue<Int>)
      
      







だった



 rx_modelSelected(Int)
      
      







そして、テーブルの行をクリックすると、別の素晴らしい間違いを見つけました。



 fatal error: Failure converting from 4 to Int: file /Users/SparkLone/projects/repos/RxSwift/RxCocoa/Common/RxCocoa.swift, line 340
      
      







はい、4はどのようにしてintになりますか。Intの代わりにどの型を使用すべきかを調べるために、ライブラリ内部の別の失敗した研究の後、このように出力することを推測しました



 tableView .rx_itemSelected .subscribeNext { [weak self] i in if let controller = self { print("Tapped `\(i)` - \(controller.sections.value[i.section].items[i.row].dynamicType)") } } .addDisposableTo(disposeBag)
      
      







私のアカウントではこのエラーを一気に受け止めることができますが、例にIdentifiableValueの記述はありません。



3)最初、最初のセクションのデータとして[1、3、5]ではなく[1、3、3]を指定しましたが、

アプリケーションは正常に起動しましたが、まったく異なるセクションの行を削除しようとすると、すでにこのエラーが発生しました



 precondition failed: Item 3 has already been indexed at (0, 1): file /Users/SparkLone/projects/repos/RxSwift/RxExample/RxDataSources/DataSources/Differentiator.swift, line 130
      
      





判明したように、テーブルの行の重複に対する保護が組み込まれているため、行の値は一意である必要があります。ご想像のとおり、これもすぐにわかりました。



すべてのエラーが一種の軽薄なものであるように見えることは明らかであり、同じレーキを2度目に踏むことはありません。しかし、テーブルを操作する簡単な例をスケッチするのに30分を費やすことを期待して、ライブラリの内部に飛び込んで、すべてが再び機能しない理由を理解することは非常に不快です。そして、デバッグの助けを借りても、実行の非線形性を考えると、問題が何であるかを理解するのは簡単(高速)ではありません。時間の経過とともに、すべての拡張機能の標準化が行われ、より詳細でわかりやすいドキュメントが作成されることを本当に望んでいます。私にとって、最初のパンケーキはゴツゴツしたものでした。



では、すべての仕組みを見てみましょう。







UIViewサブクラス上に拡張機能を作成し、その内部で変数rx_delegateを定義します。これにより、デリゲートのプロキシが作成されます。次に、拡張機能で、処理する予定のイベントのラッパーを作成します。クライアントはこれらのイベントラッパーをサブスクライブし、そのようなイベントが発生すると、プロキシはまずクライアントによって受信されるObservable要素を生成し、次に存在する場合、Rxデリゲートを作成する前に割り当てられた場合、通常のデリゲートに送信します(プロトコルAPIが明確になるように転送します) 。



プロトコルは基づいています



 protocol DelegateProxyType { //      static func createProxyForObject(object: AnyObject) -> AnyObject //      objc_setAssociatedObject static func assignProxy(proxy: AnyObject, toObject object: AnyObject) //       objc_getAssociatedObject static func assignedProxyFor(object: AnyObject) -> AnyObject? //     /    ( Rx)  func setForwardToDelegate(forwardToDelegate: AnyObject?, retainDelegate: Bool) func forwardToDelegate() -> AnyObject? //     /  -,    static func currentDelegateFor(object: AnyObject) -> AnyObject? static func setCurrentDelegate(delegate: AnyObject?, toObject object: AnyObject) }
      
      







このプロトコルの最初の5つのメソッドを実装するベースクラスDelegateProxyもあります。通常、残りの2つは特定の拡張子をオーバーライドします。UIオブジェクトのタイプと、特定のUIControlのデリゲートを含むプロパティの名前を知っています。



 class DelegateProxy { public class func createProxyForObject(object: AnyObject) -> AnyObject {} public class func assignedProxyFor(object: AnyObject) -> AnyObject? {} public class func assignProxy(proxy: AnyObject, toObject object: AnyObject) {} public func setForwardToDelegate(delegate: AnyObject?, retainDelegate: Bool) {} public func forwardToDelegate() -> AnyObject? {} }
      
      







少し明確にするために、UISearchControllerクラスを例として考えてください。

彼のために拡張機能が作成されました



 extension UISearchController { //    ,    RxSearchControllerDelegateProxy public var rx_delegate: DelegateProxy { return proxyForObject(RxSearchControllerDelegateProxy.self, self) } // Rx     UISearchControllerDelegate.didDismissSearchController(_:) public var rx_didDismiss: Observable<Void> { return rx_delegate .observe(#selector(UISearchControllerDelegate.didDismissSearchController(_:))) .map {_ in} } ... }
      
      





UISearchControllerのプロキシはRxSearchControllerDelegateProxyです



 public class RxSearchControllerDelegateProxy : DelegateProxy , DelegateProxyType , UISearchControllerDelegate { //    ( )      (UISearchController)      (delegate) public class func setCurrentDelegate(delegate: AnyObject?, toObject object: AnyObject) { let searchController: UISearchController = castOrFatalError(object) searchController.delegate = castOptionalOrFatalError(delegate) } //       ,      public class func currentDelegateFor(object: AnyObject) -> AnyObject? { let searchController: UISearchController = castOrFatalError(object) return searchController.delegate } }
      
      







もう少し掘り下げます。



例のプロキシは、次を使用して作成されます

 proxyForObject(RxSearchControllerDelegateProxy.self, self)
      
      







proxyForObjectはグローバル関数であり、デリゲートのプロキシを操作するコアです。プロキシタイプ(RxSearchControllerDelegateProxy.self)と、プロキシをアタッチするオブジェクトがパラメーターとして渡されます。



この場合、タイプはRxSearchControllerDelegateProxy、object-UISearchControllerタイプの現在のオブジェクトになります



 public func proxyForObject<P: DelegateProxyType>(type: P.Type, _ object: AnyObject) -> P { MainScheduler.ensureExecutingOnScheduler() //        let maybeProxy = P.assignedProxyFor(object) as? P // assignedProxyFor   DelegateProxy      let proxy: P if maybeProxy == nil { proxy = P.createProxyForObject(object) as! P //   (    RxSearchControllerDelegateProxy).  createProxyForObject   DelegateProxy          ,       ,        P.assignProxy(proxy, toObject: object) //     . assignProxy     DelegateProxy,    ,   assignedProxyFor assert(P.assignedProxyFor(object) === proxy) } else { proxy = maybeProxy! //       -   } let currentDelegate: AnyObject? = P.currentDelegateFor(object) //       UI  ( delegate/dataSource).     DelegateProxy   , ..    as!         if currentDelegate !== proxy { //        proxy.setForwardToDelegate(currentDelegate, retainDelegate: false) //           .    UI        .     Objective-C  Rx,    _RXDelegateProxy. P.setCurrentDelegate(proxy, toObject: object) //     DelegateProxy   , ..    as!         assert(P.currentDelegateFor(object) === proxy) assert(proxy.forwardToDelegate() === currentDelegate) } return proxy }
      
      







したがって、この関数は、まだ作成されていない場合はプロキシを作成し、現在のデリゲートとして配置します。作成した場合は、通常のデリゲートを保存します。



実装に深く入りたくありません。デリゲートメソッドを呼び出すときのメソッド置換は、Objective-Cコードからの標準スウィズルによって行われるとしか言えません。



UMLシーケンス図を作成しましたが、プロキシがどのように作成されるかが少し明確になることを願っています(写真をクリックできます)。そして、もう少し深く潜り、最後に約束します。UIクラスにデリゲートがありますが、デリゲートもある別のUIクラスからの継承である場合はどうなりますか?ファクトリメソッドが役立ちます。













UITableViewの例を見てみましょう。UIScrollViewの子孫であり、デリゲートも持っています。したがって、rx_delegateは、UITableViewではなく親クラス(UIScrollView)で定義されます。

RxTableViewDelegateProxyのプロキシは、RxScrollViewDelegateProxyの後継です。



 extension UIScrollView { /** Factory method that enables subclasses to implement their own `rx_delegate`. - returns: Instance of delegate proxy that wraps `delegate`. */ public func rx_createDelegateProxy() -> RxScrollViewDelegateProxy { return RxScrollViewDelegateProxy(parentObject: self) } /** Reactive wrapper for `delegate`. For more information take a look at `DelegateProxyType` protocol documentation. */ public var rx_delegate: DelegateProxy { return proxyForObject(RxScrollViewDelegateProxy.self, self) } ... }
      
      







プロキシでは、createProxyForObjectクラスメソッドがオーバーライドされ、プロキシの作成がrx_createDelegateProxyメソッドに委任されます。



 public class RxScrollViewDelegateProxy : DelegateProxy , UIScrollViewDelegate , DelegateProxyType { public override class func createProxyForObject(object: AnyObject) -> AnyObject { let scrollView = (object as! UIScrollView) return castOrFatalError(scrollView.rx_createDelegateProxy()) } ... }
      
      







UItableViewでは、rx_createDelegateProxyメソッドがオーバーライドされます



 extension UITableView { /** Factory method that enables subclasses to implement their own `rx_delegate`. - returns: Instance of delegate proxy that wraps `delegate`. */ public override func rx_createDelegateProxy() -> RxScrollViewDelegateProxy { return RxTableViewDelegateProxy(parentObject: self) } ... }
      
      







RxTableViewDelegateProxyコンストラクターは、作成時に親コンストラクターを呼び出します(この例では、RxScrollViewDelegateProxy)



 public class RxTableViewDelegateProxy : RxScrollViewDelegateProxy , UITableViewDelegate { public weak private(set) var tableView: UITableView? public required init(parentObject: AnyObject) { self.tableView = (parentObject as! UITableView) super.init(parentObject: parentObject) } }
      
      







このようにして、プロキシチェーン全体が初期化されます。



次のUMLスキームは楽しいもので、継承を考慮したプロキシの作成と割り当てがどのように行われるかを示しています(写真をクリックできます)。







まとめると。RxSwiftは非常に興味深いトピックですが、プロジェクトの現在の状態には粗さがあります。これがどのように機能するかを理解したら、アーキテクチャのフレームワーク内でそれを正しく適用する方法について考える必要があります。



じゃあそのような記事を書くことは、一方でその資料を理解することをより深くし、他方ではかなりの時間を要します。このトピックについて書き続けることは理にかなっていますか?

残念ながら、私は自分自身を「銀の弾丸」を探している人としてではなく、厳しい建築家として位置づけることはできません。そのため、私の結論は明らかであり、間違っています。

すべてのエラーについて、いつものように-PMで。



All Articles