Search code examples
swiftrx-swiftreactiveswift-extensionseureka-forms

How to make a proper reactive extension on Eureka SelectableSection


This is my first question to the StackOverflow community so excuse me if I'm doing something wrong.

1. What I'm trying to achieve
Basically, I want to make a custom reactive wrapper around Eureka's SelectableSection class in order to observe the value of the selected row when it is changed. I'm thinking to get this data from the onSelectSelectableRow closure which is called every time a row is selected.

2. What I've tried to do for that
Actually, I've got this working but it's not a generic use of the custom wrapper, here is the example that works but only when I specify the row and its value type, for example ListCheckRow<Int>.

extension SelectableSection: ReactiveCompatible {}
extension Reactive where Base : SelectableSection<ListCheckRow<Int>> {
  var selectedValue: Observable<Base.SelectableRow.Cell.Value?> {
    return Observable.create { observer in
      self.base.onSelectSelectableRow = {cell, row in
        observer.onNext(row.value)
      }
      return Disposables.create {
        observer.onCompleted()
      }
    }
  }
}

This works fine and as I expected but when it comes to something more generic like the next code example, I get an error saying that: "Cannot assign to property: 'base' is a 'let' constant"

extension SelectableSection: ReactiveCompatible {}
extension Reactive where Base : SelectableSectionType {
  var selectedValue: Observable<Base.SelectableRow.Cell.Value?> {
    return Observable.create { observer in
      self.base.onSelectSelectableRow = {cell, row in // Error: Cannot assign to property: 'base' is a 'let' constant
        observer.onNext(row.value)
      }
      return Disposables.create {
        observer.onCompleted()
      }
    }
  }
}

Any help will be much appreciated, thanks. 🙏


Solution

  • The fundamental problem here is that SelectableSectionType is a protocol that isn't restricted to class types and Reactive assumes that Base is a class (or otherwise is not going to be modified by the observable creation.)

    I think the most generic you can make this is something like:

    extension Reactive {
        func selectedValue<Row, T>() -> Observable<T?> where Base: SelectableSection<Row>, Row: SelectableRowType, T == Row.Cell.Value  {
            Observable.create { [base] observer in
                base.onSelectSelectableRow = { cell, row in
                    observer.onNext(row.value) // this is problematic. See below.
                }
                return Disposables.create {
                    observer.onCompleted() // this is wrong. See below.
                }
            }
        }
    }
    

    The biggest problem with the above though is that if you subscribe to the resulting Observable more than once or create more than one Observable using this computed property, all but the last subscription will silently fail. The simple way to fix this is to always remember to share any result but that's rather error prone.

    The way to fix this would be to associate a Subject with each SelectableSection, but you can't modify the class, so what are we to do?

    Here's a solution:

    extension Reactive {
        func selectedValue<Row, T>() -> Observable<T?> where Base: SelectableSection<Row>, Row: SelectableRowType, T == Row.Cell.Value  {
            Observable.create { [base] observer in
                if let block = selectableSections.first(where: { $0.section === base }) {
                    let subject = block.subject as! PublishSubject<T?>
                    return Disposables.create(
                        block.disposable.retain(),
                        subject.subscribe(observer)
                    )
                }
                else {
                    let subject = PublishSubject<T?>()
                    let block = SelectableSectionBlock(
                        section: base,
                        subject: subject,
                        disposable: RefCountDisposable(disposable: Disposables.create {
                            selectableSections.removeAll(where: { $0.section === base })
                        })
                    )
                    base.onSelectSelectableRow = { cell, row in
                        subject.onNext(row.value)
                    }
                    selectableSections.append(block)
                    return Disposables.create(
                        block.disposable,
                        subject.subscribe(observer)
                    )
                }
            }
        }
    }
    
    private struct SelectableSectionBlock {
        let section: Section
        let subject: Any
        let disposable: RefCountDisposable
    }
    
    private var selectableSections = [SelectableSectionBlock]()
    

    The selectableSections array stores a Subject and RefCountDisposable for each SelectableSection.

    Whenever an Observable is created, or subscribed to...

    • if it's the first time working with this section, it will create a Subject and RefCountDisposable assign the onSelectSelectableRow to send a next event to the subject and store the subject in the array.
    • otherwise it will find the subject and disposable associated with this Section and retain the disposable.

    Once it has the subject and disposable from above, it will subscribe the new observer to the subject and return a new Disposable that will remove that subscription and decrement the ref-count when the time comes.

    Yes this is quite a bit more complex than the simple assignment case, but it's the right thing to do.

    As for calling onCompleted() inside the disposable closure. By the time the closure is called, the observer has already emitted an onCompleted/onError event, or the observer has stopped listening to the observable. So this event will never be seen.