📜 ⬆️ ⬇️

RxSwift: working with GUI



My first article on RxSwift covered almost all the basic operators, without whose knowledge it wasn’t much sense to go into development. But this is just the functional programming alphabet. In order to write full-fledged programs, it is necessary to understand the basic principles when working with GUI.

Basically, standard materials from RxExample are used to work through the material, but UIExplanation sandbox and an additional example in RxExample were created to clarify certain points.
All code can be found here github.com/sparklone/RxSwift
')
When working with UI elements in Rx there are basic needs:
1) understand what pitfalls await us in principle and why you need a driver
2) learn how to bind UI to Observable so that Observable elements change the state of the UI property / properties of the element. This is solved using UIBindingObserver
3) learn how to translate the target-action pattern on the Rx rails. This is done using ControlEvent.
4) make a two-way binding to the properties of the UI element. This is done using the ControlProperty.
5) because Often, the UI elements of delegate / dataSource are assumed to be singular, they introduced the class DelegateProxy, which allows you to use both a normal delegate and Rx sequences at the same time.

Consider each need separately



Driver



1) There are several problems with Observable. To understand them, consider a small example in the sandbox.

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) If there is an Observable and to sign several Observers for it, a separate Observable will be created for each Observer. In our case, Observable addresses the network and downloads the page, the page code and the status of the server response are placed in a different textView.

If we look at the console, we will see 2 subscribed, 2 disposed:
 --- 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 


To avoid this, you need to add shareReplayLatestWhileConnected to the initial observable
 let observable = NSURLSession.sharedSession().rx_response(request).debug("http").shareReplayLatestWhileConnected() 


As a result, the console shows that now there is only one call to the server.
 --- 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 


I also note that shareReplayLatestWhileConnected is used, not shareReplay (1), because it clears the buffer when unsubscribing all Observers and completing the sequence correctly or with an error. When I wrote the first article on RxSwift operators, I discovered this strange behavior of shareReplay (lack of cleaning even after the sequence was completed) on my own in the sandbox and first decided that I was doing something wrong, it turned out - by design.

b) we are obliged to process everything related to the GUI on MainScheduler. If you need to refresh your memory about different Schedulers. You can refer to the official documentation and follow the links where I described subscribeOn and observeOn in the previous article.
If we remove .observeOn (MainScheduler.instance) from the code, we get
 fatalError "fatal error: Executing on backgound thread. Please use `MainScheduler.instance.schedule` to schedule work on main thread." 

By the way, I was somewhat puzzled by this error, because I knew that in what stream you create the Observable, the code inside it will be executed in this one. But I mistakenly thought that in what stream the call to subscribe goes, in such a way the execution of the observer code will occur.

At the first moment, the Observable code is actually executed in the same thread where it was created. But, in the case of rx_response, an Observable is created inside, inside of which there is a call to the NSURLSession dataTaskWithRequest method, and the return of values ​​comes from the closure of this method, and this closure is already running in a completely different stream. Therefore, at the output of NSURLSession.sharedSession (). Rx_response (request) another thread is waiting for us.

And on the second point - after reading the official documentation, I mistakenly thought that from which thread you were calling subscribe - the Observer body would be executed in this thread, it turned out that it wasn’t. The stream is saved in which the executable Observable code is located.
To check this, I wrote two more examples.

 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())") } }) } 


Output to console:

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


In both examples, I do not use observeOn. As you can see in both cases, the code inside the observer is executed not in the flow of the code that made subscribe, but in that it returned from rx_response (you can make sure of this by logging the flows within the NSURLSession + Rx file from the Rx project)

 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) if an error occurs while processing the Observable code, then in the Debug mode we catch fatalError, and in Release, the error “Binding error to UI: Argument out of range.” goes to the console and the entire UChief involved in the UI goes to the Observable.

To check how this happens - I modified the original IntroductionExampleViewController a bit. I commented out the binding to disposeButton.rx_tap, instead I made my own (I commented out my version on the githaba so that you can change the implementation on the fly)

 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) 


In Release mode in the console at startup appears
 2016-04-30 02:02:41.486: rx_tap -> subscribed 


And when you first press the button

 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 


Further button presses do not lead to anything, since rx_tap has become disposed

As a result, in order not to follow these moments, Driver was created, it guarantees just three things.
a) data will be scrambled using shareReplayLatestWhileConnected
b) the stream is executed on MainScheduler (roughly speaking the UI stream)
c) no errors will be generated (we ourselves decide what value to return instead of an error)

Thus, the creation of the driver can be represented as it is done in the official documentation.

 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 


If we see somewhere drive () instead of subscribe () we understand that we can safely work with ui.

Consider now an example of GitHubSignup, there just compare the head-on code with the use of Driver and without it.
Without using the Driver, the creation code of the viewModel is as follows:

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

because rx_text is ControlProperty, then asObservable returns an internal Observable without any conversions.

Now as will be with the use of the driver'a

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

The difference is small, instead of asObservable - asDriver, which leads to the fulfillment of the above 3 conditions.

If you take the application, the difference is also minimal; without Driver, subscribe / bind and their modifications are used.

 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) 


With driver we use drive and its modifications

 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) 

A little more interesting will be to look at GithubSignupViewModel1 / GithubSignupViewModel2 where the drivers are created

Verbose code in GithubSignupViewModel1

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


simplified to

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


The sensible use of this knowledge should already protect against major errors when working with UI. But still this is not enough, you need to understand how the standard UI elements for working with Rx are expanded, in order to write your own extensions if necessary.



UIBindingObserver


2) using the example of GeolocationViewController.swift, you can see how to hang your own Observers on the UI elements

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


So, UIBindingObserver is a generic helper class that allows you to bind a parameter passed to the closure (in our case, location) to changes in the property / properties of the passed object (in our case, the text property). UIBindingObserver is parameterized by the object class (in our case, UILabel, because extension UILabel), the parameters will be passed to the closure as the object itself (label) and the value with which we will change the state of the object (location)
The type for the location parameter in this example is determined by the automaton, due to the parameterization of the return value AnyObserver <CLLocationCoordinate2D>
This code for example will not work.

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

After all, at the time of the creation of observer, - UIBindingObserver has no idea what type the location will have, because unlike the original, it does not immediately return from the closure. The "magic" of autodetection types will not work.
But this will go, because we explicitly specified the type of all parameters when creating the 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() } 


To summarize On the one hand, this remark is not directly related to RxSwift, it is rather a reference to how Swift works with the types of transferred values ​​and their automatic recognition, which saves us from the routine explicit indication of types. On the other hand, it is important to understand that there is no magic in the RXSwift binders. With the knowledge that where it comes from and transmitted from, it is possible to come up with a puzzle to fix, for example, we want the text color of UILabel to change color depending on the value of the Bool type parameter transmitted to the closure. If it is true, let the text color turn red, and black if false
All that is needed is to parameterize the type returned by the definition of Observer by the Bool type, and it is correct to use this knowledge inside the closure

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


Well, last thing, why don't we return the UIBindingObserver, why bring to AnyObserver? Because otherwise we would have to parameterize the type of the return value by the type of the object (UILabel), which is absolutely not important in the context of the task.

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


Are we right? We look into the definition of AnyObserver

/ **
A type-erased ObserverType.

Forwarding operations of an observer type.
* /
So it is, AnyObserver is a wrapper that hides the type of the transferred object, leaving only the type of the parameter passed to the closure.

The following extension, thanks to the knowledge gained, is easy to read. Depending on the Bool type parameter passed to the closure, we hide the UIView, or vice versa, make it visible.

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



ControlEvent



3) To process the target-event pattern in the Rx environment, enter the ControlEvent <> structure
It has the following properties:
- its code will never fall
- no initial value will be sent when subscribing
- when memory is released, the control will generate .Completed
- no errors will ever come out
- all events will be executed on MainScheduler

Consider the example of clicking on a simple button. For UIButton, an extension is created where the rx_tap property is defined.
 extension UIButton { /** Reactive wrapper for `TouchUpInside` control event. */ public var rx_tap: ControlEvent<Void> { return rx_controlEvent(.TouchUpInside) } } 


For UIControl, the method is defined in the extension

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


Inside the ControlTarget class, the event is already subscribed
control.addTarget (self, action: selector, forControlEvents: controlEvents)

Using the same extensions is as easy as regular Observable.
Consider the example of GeolocationExample, or rather the class GeolocationViewController

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


Here we simply do bindNext for each click on the button, and in the circuit code, open the settings panel.
bindNext by the way is just a wrapper over the subscribe with a check that we are in the main thread

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


We can also, at any time, if necessary, obtain from ControlEvent - Observable using .asObservable () or Driver using .asDriver ()


Controlproperty



4) To make a two-way binding to the UI properties of the element, the ControlProperty <> structure with the following properties comes to the rescue

- its code will never fall
- on the sequence of elements applied shareReplay (1)
- when freeing by memory control will be generated .Completed
- no errors will ever come out
- all events will be executed on MainScheduler

For example, of course, let's take the text property from UITextField

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


Let's see what the rx_value method is.

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


As we can see, two-way binding is a combination of the ControlTarget and UIBindingObserver already discussed.
If you look at the definition of ControlProperty, you can see that it implements the ControlPropertyType protocol, which in turn inherits from both ObservableType and ObserverType.
Take another look at the code 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) } 


If we need both behaviors at the same time, i.e. do two-way binding - you can see how to create your own operator in Rx code

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


The operator allows you to create a binding simply and clearly

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



DelegateProxy



5) The cornerstone of Cocoa architecture is delegates. But it is usually assumed - one delegate for one object, so the DelegateProxy class was added to Rx, which allows you to use both a regular delegate and Rx sequences at the same time.

From the point of view of the user of the existing API, there is nothing particularly complicated and nothing.
Take for example the UISearchBar, we want to somehow react to pressing the Cancel button. A variable has been created for us in the extension for the UISearchBar class.

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


Working with her is easy and simple:
 searchBar.rx_cancelButtonClicked.subscribeNext { _ in //    } 


But working with tableView somewhat upset me.
If you take an example (SimpleTableViewExample), then everything is simple

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


Cool, rx_itemsWithCellIdentifier is defined in Rx itself, so it is accessible to all. OK. And how are things with the table with sections? Let's see an example of 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 } } 


Pay attention to the RxTableViewSectionedReloadDataSource, and where is it defined? In the RxExample project, i.e.as I understand it is not a run-in solution that is recommended to everyone, but for example. If you look inside, you understand why, they propose to reload data for the entire table for every sneeze.

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

To put it mildly, not the best solution. What are the alternatives? Again, RxTableViewSectionedAnimatedDataSource is defined in RxExample. For an example of working with this dataSource, an example of TableViewPartialUpdates is provided. It demonstrates in comparison how to update data in tables with sections with both a full data reload (RxTableViewSectionedReloadDataSource) and a partial data reload (RxTableViewSectionedAnimatedDataSource). Here is also an example of working with CollectionView. But all this without regard to the possibility of editing.
Well, me and the cards in hand, I will create a simple example of working with a table with sections and the possibility of editing. The example TableViewEditPartialUpdate I put to the rest of the examples in RxExample.

Considering that this was my first experience of the “combat” code for working with GUI in RxSwift, I immediately received my portion of the rake.

 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) I wrote this code, I registered my class for the TableViewController created in the storyboard and tried to run it. Mistake.
 fatal error: Failure converting from <RxExample_iOS.TableViewControllerEditPartialUpdate: 0x7f895a643dd0> to UITableViewDataSource: file /Users/SparkLone/projects/repos/RxSwift/RxCocoa/Common/RxCocoa.swift, line 340 

Wow. Not immediately, I realized what was happening. I picked up a lot of Rx code in a vain attempt to get through the wilds. And the point was that by default, when creating a ViewTableController in the designer, it specifies our controller as a dataSource. And when Rx creates a proxy, it tries to specify the current dataSource as forwardToDelegate. And my controller does not implement the DataSource in the canonical form. Of course, there is no one to blame, but apparently starting to work with a library of this kind, subconsciously expecting some tricky bugs.

2) Okay, they wanted tricky bugs - please.
Initially instead of string

 rx_modelSelected(IdentifiableValue<Int>) 


was

 rx_modelSelected(Int) 


and when I clicked on a table row, I caught another great mistake.

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


Well yes, how does 4 lead to int then. After another unsuccessful study of the library's entrails, to figure out which type should be instead of Int, I guessed to derive it in this way.

 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) 


I can take this error into my account with a stretch, nowhere was there any mention of any IdentifiableValue in the examples.

3) I initially indicated that the data for the first section was not [1, 3, 5] but [1, 3, 3]. The
application started normally, but when I tried to delete a line in a completely different section, I received this error

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

As it turned out, protection against duplicates in the rows of the table is built in, the values ​​for the rows must be unique. As you might guess, I did not find out immediately either.

It is clear that all the errors seem to be some kind of frivolous, and the second time you will not step on the same rake. But hoping to spend half an hour to sketch a simple example of working with a table, it is extremely unpleasant to dive into the inside of the library in order to understand why everything does not work once again. And taking into account the non-linearity of execution, even with the help of debugging, it is not so easy (quickly) to understand what's the matter. I really hope that over time, the standardization of all extensions will be carried out, more detailed and intelligible documentation will be written. For me, the first pancake was lumpy.

Well, let's look at how it all works.



We create an extension above the UIView subclass, inside it we define the variable rx_delegate, which in turn creates a proxy for the delegate. Further, in the extension we write the wrappers over the events that we plan to process. The client subscribes to these wrappers over the events, and when such an event occurs, the proxy first generates an Observable element that arrives to the client, then if there is, sends (makes forward to understand the protocol API) to its normal delegate if it was assigned before creating the Rx delegate .

The basis is the protocol

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


There is also a base class DelegateProxy which implements the first 5 methods of this protocol. The remaining two usually override specific extensions, since they know what type the UI object should be and what name has the property containing the delegate in the particular 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? {} } 


To make it a little clearer, consider the example of the UISearchController class.
An extension has been created for it.

 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} } ... } 

Proxy for UISearchController is 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 } } 


Dig a little deeper.

The proxy in the example is created using
 proxyForObject(RxSearchControllerDelegateProxy.self, self) 


proxyForObject is a global function, the core of proxies for delegates. As parameters, the proxy type (RxSearchControllerDelegateProxy.self) and the object to which we will attach the proxy are passed to it.

In our case, the type will be RxSearchControllerDelegateProxy, object - the current object of type 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 } 


Thus, this function creates a proxy if it was not created earlier, puts it in as the current delegate, and if it was, it retains the normal delegate.

I don’t really want to go very deep into the implementation, I’ll just say that the method substitution when calling delegate methods is done with a standard swizzling from Objective-C code.

I created a UML sequence diagram, I hope it will become a little clearer with it how the proxy is created (the image is clickable). And now we dive a little deeper, for the last time, I promise. What to do if our UI class has a delegate, but it is a successor from another UI class that also has a delegate? The factory method will help us.






Consider the example of UITableView. He is the heir of UIScrollView, and of which there is also a delegate. Therefore, rx_delegate is defined in the parent class (UIScrollView), not in the UITableView.
The proxy for RxTableViewDelegateProxy is derived from 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) } ... } 


In his proxy, the method of the class createProxyForObject is redefined, which delegates the creation of the proxy to the method 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()) } ... } 


In the UItableView, the rx_createDelegateProxy method is overridden

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


The RxTableViewDelegateProxy constructor calls the parent constructor when it is created (in our case, 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) } } 


Thus, the entire proxy chain is initialized.

The next UML scheme, as far as possible, can be seen on it as the creation and assignment of a proxy takes place, taking into account inheritance (the image is clickable).



Summarize.RxSwift theme is very interesting, although the current state of the project and not without rough edges. Having understood how it works, you need to think about how to correctly apply it within the framework of the architecture.

Well.Writing such articles, on the one hand, makes you understand the material more deeply; on the other hand, it takes a considerable amount of time. Does it make sense to continue writing on this topic?
Unfortunately, I cannot position myself as a cool architect, rather as a person in search of a “silver bullet”, so my conclusions can be both obvious and incorrect, but the road can be reached by walking.
About all errors as always - in PM.

Source: https://habr.com/ru/post/283128/


All Articles