简体   繁体   中英

How to convert `Observable` to `ControlEvent`

We have a view controller MainCollectionView that contains a collection view with a number of cells FooCell . And inside each FooCell , there is a collection view and a list of cells BarCell .

How do I propagate a button tapped event in the BarCell to MainCollectionView ?

This is what we have:

class FooCell: ... {

    private let barCellButtonTappedSubject: PublishSubject<Void> = PublishSubject<Void>()
    var barCellButtonTappedObservable: Observable<Void> {
        return barCellButtonTappedSubject.asObserver()
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeue(...)

        if let cell = cell as BarCell {
            cell.button.rx.tap.bind { [weak self] in
                self?.barCellButtonTappedSubject.onNext(())
            }.disposed(by: cell.rx.reusableDisposeBag)
        }

        return cell
    }
}

class MainCollectionView: ... {

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeue(...)

        if let cell = cell as FooCell {
            cell.barCellButtonTappedObservable.subscribe { [weak self] in
                // Do other stuff when the button inside bar cell is tapped.
            }.disposed(by: cell.rx.reusableDisposeBag)
        }

        return cell
    }
}

This works until I read about ControlEvent :

  • it never fails
  • it won't send any initial value on subscription
  • it will Complete sequence on control being deallocated
  • it never errors out
  • it delivers events on MainScheduler.instance

It looks like it is more appropriate to use ControlEvent in the FooCell :

private let barCellButtonTappedSubject: PublishSubject<Void> = PublishSubject<Void>()
var barCellButtonTappedObservable: Observable<Void> {
    return barCellButtonTappedSubject.asObserver()
}

What is the right way to convert this barCellButtonTappedObservable to a ControlEvent ? Or is there other better idea to propagate the ControlEvent in the nested cell to the outer view controller?

I personally prefer using RxAction for this kind of stuff, but because you have already declared a PublishSubject<Void> in your cell, this is how you can convert a subject to ControlEvent

controlEvent = ControlEvent<Void>(events: barCellButtonTappedSubject.asObservable())

As straight forward as it can get! but if thats all you wanna do, you don't even need a barCellButtonTappedSubject

controlEvent = ControlEvent<Void>(events: cell.button.rx.tap)

In fact, you don't even need to declare a control event :) because cell.button.rx.tap itself is a control event :) So if you declare your button as public property in your cell, you can directly access its tap control event in your tableView controller

But personally, I would use RxAction rather than declaring a publishSubject or controlEvent your FooCell can ask for action from your TableViewController

class FooCell: ... {
   var cellTapAction : CocoaAction! = nil

   func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeue(...)

        if let cell = cell as BarCell {
            cell.button.rx.action = cellTapAction
        }

        return cell
    }
}

Finally your TableViewController/CollectionViewController can pass action as

class MainCollectionView: ... {

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeue(...)

        if var cell = cell as FooCell {
            cell.cellTapAction = CocoaAction { _ -> Observable<Void> in
               debugPrint("button in cell tapped")
               return Observable.just(())
           }
        }

        return cell
    }
}

Only thing you would have to handle is if cellctionView is embedded inside FooCell because am passing action after deQueReusableCell embedded collectionView might load even before action is passed to it so you will have to tweak the logic to either reload the embedded collection view after action passed to FooCell or any other workaround which will solve this issue :)

Hope it helps :) I believe using Action makes code cleaner and easy to understand.

I've never used Action s which the other answers have mentioned. I also wonder why you seem to be manually setting up your delegate instead of using RxCocoa to do it. Lastly, it feels like you probably want some way of knowing which button was tapped. I do that in the code below by assigning each Bar cell an ID integer.

class BarCell: UICollectionViewCell {

    @IBOutlet weak var button: UIButton!

    func configure(with viewModel: BarViewModel) {
        button.rx.tap
            .map { viewModel.id }
            .bind(to: viewModel.buttonTap)
            .disposed(by: bag)
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        bag = DisposeBag()
    }

    private var bag = DisposeBag()
}

class FooCell: UICollectionViewCell {

    @IBOutlet weak var collectionView: UICollectionView!

    func configure(with viewModel: FooViewModel) {
        viewModel.bars
            .bind(to: collectionView.rx.items(cellIdentifier: "Bar", cellType: BarCell.self)) { index, element, cell in
                cell.configure(with: element)
        }
        .disposed(by: bag)
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        bag = DisposeBag()
    }

    private var bag = DisposeBag()
}

class MainCollectionView: UIViewController {

    @IBOutlet weak var collectionView: UICollectionView!

    var viewModel: MainViewModel!

    override func viewDidLoad() {
        super.viewDidLoad()

        let foos = viewModel.foos
            .share()

        let buttonTaps = foos
            .flatMap { Observable.merge($0.map { $0.bars }) }
            .flatMap { Observable.merge($0.map { $0.buttonTap.asObservable() }) }

        buttonTaps
            .subscribe(onNext: {
                print("button \($0) was tapped.")
            })
            .disposed(by: bag)

        foos
            .bind(to: collectionView.rx.items(cellIdentifier: "Foo", cellType: FooCell.self)) { index, element, cell in
                cell.configure(with: element)
            }
            .disposed(by: bag)
    }

    private let bag = DisposeBag()
}

struct FooViewModel {
    let bars: Observable<[BarViewModel]>
}

struct BarViewModel {
    let id: Int
    let buttonTap = PublishSubject<Int>()
}

struct MainViewModel {
    let foos: Observable<[FooViewModel]>
}

The most interesting bit about the code was the merging up of all the buttonTaps. That was a bit of an adventure to figure out. :-)

I would personally add an action to the button before trying to observe its state like that and handle bubbling up your response from there.

class Cell: UICollectionViewCell {
   let button = UIButton(type: .custom)

   func setUpButton() {
      button.addTarget(self, action: #selector(Cell.buttonTapped), for: .touchUpInside)
   }

   @IBAction func buttonTapped(sender: UIButton) {
      //This runs every time the button is tapped
      //Do something here to notify the parent that your button was selected or handle it in this Cell.
      print(sender.state)
   }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM