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.