简体   繁体   中英

Button tap in UICollectionViewCell toggles state in another cell also

I have a UICollectionViewCell that contains a UIButton .

When tapping the button in a cell, I want to toggle the selected state.

This works to an extent, the bug I am seeing is, clicking in 1 cell toggles the state in another cell also.


import UIKit
import RxSwift

class PersonaliseYourAppsListCell: BaseCollectionViewCell<Launcher> {

    let toggleSelectedTrigger = PublishSubject<String>()

    private let disposeBag = DisposeBag()

    override init(frame: CGRect) {
        super.init(frame: frame)

        configureSubviews()
    }

    required init?(coder: NSCoder) {
        return nil
    }

    override func render(with model: Launcher?) {
        guard let model = model else { return }

        title.text = model.name
        icon.image = model.icon

        toggleButton.rx.tap.bind { [weak self] in
            self?.toggleButton.isSelected.toggle()
            self?.toggleSelectedTrigger.onNext(model.id)
        }.disposed(by: disposeBag)

        if let status = model.status {
            toggleButton.isSelected = model.selected ?? false
            toggleButton.isUserInteractionEnabled = status != .forced

            [title, icon, toggleButton].forEach {
                $0.alpha = status == .forced ? 0.6 : 1
            }

            if status == .forced {
                addSubviews(enforcedLabel)
                enforcedLabel.position(top: title.bottomAnchor, leading: title.leadingAnchor, withPadding: .zero)
            }
        }
    }

    override func prepareForReuse() {
        super.prepareForReuse()
        enforcedLabel.removeFromSuperview()
    }

    // MARK:- UI Elements

    private func configureSubviews() {
        addSubviews(icon, title, toggleButton, separatorView)

        icon
            .isCenteredY()
            .position(
                leading: leadingAnchor,
                withPadding: .init(top: 0, left: 16, bottom: 0, right: 4)
        )

        title
            .isCenteredY()
            .position(
                leading: icon.trailingAnchor, trailing: toggleButton.leadingAnchor,
                withPadding: .init(top: 0, left: 8, bottom: 0, right: 8)
        )

        toggleButton
            .isCenteredY()
            .withSize(.init(width: 44, height: 44))
            .position(trailing: trailingAnchor, withPadding: .init(top: 0, left: 0, bottom: 0, right: 16))

        separatorView
            .isCenteredX()
            .withSize(.init(width: frame.width - 48, height: 1))
            .position(bottom: bottomAnchor)
    }

    private lazy var icon: UIImageView = {
        let iv = UIImageView(frame: .zero)
        iv.contentMode = .scaleAspectFit
        [iv.widthAnchor, iv.heightAnchor].forEach { $0.constraint(equalToConstant: 64).isActive = true }
        return iv
    }()

    private lazy var title: UILabel = {
        let label = UILabel(frame: .zero)
        label.font = .systemFont(ofSize: 20)
        label.textAlignment = .left
        label.numberOfLines = 2
        label.textColor = .usingHex("444444")
        return label
    }()

    private lazy var toggleButton: CheckBoxButton = {
        let button = CheckBoxButton(type: .custom)
        return button
    }()

    private lazy var enforcedLabel: UILabel = {
        let label = UILabel(frame: .zero)
        label.text = "This app cannot be removed"
        label.font = .systemFont(ofSize: 12)
        label.textColor = .lightGray
        return label
    }()

    private lazy var separatorView: UIView = {
        let view = UIView()
        view.backgroundColor = UIColor(white: 0.3, alpha: 0.1)
        return view
    }()
}

My CollectionView is setup as follows:

    override func viewDidLoad() {
        super.viewDidLoad()

        presenter?.viewIsReady.onNext(())

        presenter?.data.bind(to: customView.collectionView.rx.items) { [weak self] (cv, row, item) -> UICollectionViewCell in

            let cell = cv.dequeueReusableCell(withClass: PersonaliseYourAppsListCell.self, for: .init(row: row, section: 0))
            cell.render(with: item)

            if let self = self, let presenter = self.presenter {
                cell.toggleSelectedTrigger.bind(to: presenter.updateUserAppsTrigger).disposed(by: self.disposeBag)
            }

            return cell

        }.disposed(by: disposeBag)

        customView.configureLayout()
    }

EDIT

I've noticed if I do not make the api call and just let the buttons state toggle, the bug does not exist. I wonder if the response is reloading the collection view and due to re use the wrong button state is being updated.

EDIT 2

If I trigger the API call by selecting the cell instead of the button everything works, However I would like this to happen on button click.

I suspect still this is something todo with button state.

在此处输入图像描述

In collectionView your cells will be reused. In your code, the only place where you update the selection state of your toggleButton is inside your if let status = model.status condition. There are probability that your status value might have been nil and so that the selection state of the toggleButton retained from the cell which have been reused. Can you make sure that it is not the problem by modifying your code as below.

override func render(with model: Launcher?) {
        ...

        toggleButton.rx.tap.bind { [weak self] in
            self?.toggleButton.isSelected.toggle()
            self?.toggleSelectedTrigger.onNext(model.id)
        }.disposed(by: disposeBag)

        if let status = model.status {
            toggleButton.isSelected = model.selected ?? false
            ...
        } else {
            toggleButton.isSelected = false
        }
    } 

As Subramanian Mariappan suggests, it might be because of cells being reused. In your render method you subscribe to button tap and you never actually stop observing. Replace:

private let disposeBag = DisposeBag()

with:

private var disposeBag = DisposeBag()

and in func prepareForReuse() add creation of a new disposeBag (which will release the old one by the way).

    override func prepareForReuse() {
        super.prepareForReuse()
        enforcedLabel.removeFromSuperview()

        disposeBag = DisposeBag()
    }

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