-
Notifications
You must be signed in to change notification settings - Fork 0
셀 재사용에 따른 중복 binding 이슈(feat.disposeBag)
-
Diffable datasource를 통해서 collection view를 구현하던 중, collection view의 아이템이 많아지니 collection view에 binding된 여러 요소들에게서 예상치 못한 에러가 발생했다.
-
먼저 에러가 생기는 기존의 코드를 확인해보자.
self.datasource = UICollectionViewDiffableDataSource(
collectionView: self.collectionView,
cellProvider: { [weak self ] collectionView, indexPath, item in
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: TicketListCollectionViewCell.className,
for: indexPath
) as? TicketListCollectionViewCell else { return UICollectionViewCell() }
cell.setData(with: item)
if item.ticketStatus == .notUsed {
self?.bindQRCodeExpandView(cell, with: item)
}
return cell
})
///
private func bindQRCodeExpandView(_ cell: TicketListCollectionViewCell, with item: TicketItemEntity) {
let qrCodeImageView = cell.ticketInformationView.qrCodeImageView
let ticketName = item.ticketName
qrCodeImageView.rx.tapGesture()
.when(.recognized)
.asDriver(onErrorDriveWith: .never())
.drive(with: self) { owner, _ in
guard let QRCodeImage = qrCodeImageView.image else { return }
let viewController = owner.qrExpandViewControllerFactory(QRCodeImage, ticketName)
viewController.modalPresentationStyle = .fullScreen
owner.present(viewController, animated: true)
}
.disposed(by: self.disposeBag)
}
-
위 코드는 cell의 qrCodeImageView에다가 tapGesture를 붙히는 과정이다. 그래서 cell의 qrCodeImageView를 탭하는 제스처를 핸들링할 수 있었다.
-
근데, collection view의 데이터가 많아지니 cell의 qrCodeImageView를 탭하면 원하는 정보를 띄어주는 것이 아니라, 다른 cell의 정보를 띄어주는 것을 알 수 있었다. 또한, 탭을 한번만 했는데도 탭 체스처가 여러 번 입력되는 에러 또한 발생했다.
-
일단, 다른 정보가 띄어진다는 부분에서 무조건 셀의 재사용 이슈 때문이라는 것을 짐작할 수 있었다.
-
아래의 그림과 같이 CollectionView나 TableView는 매 cell을 새로 생성하지 않는다. 화면에서 지워진 cell을 queue에 집어넣고, 화면에서 나타날 cell을 queue에서 dequeue를 하여 활용한다. 그래서 우리는 Cell의 prepareForReuse를 활용하여 cell의 내용들을 초기화하고, setData를 통해서 원하는 데이터를 보여준다.
- 또한, 아래의 코드에서 qrCodeImageView의 탭 제스처 구독의 결과 값인 disposable을 self(view controller)의 diseposeBag에 넣는다.
qrCodeImageView.rx.tapGesture()
.when(.recognized)
.asDriver(onErrorDriveWith: .never())
.drive(with: self) { owner, _ in
guard let QRCodeImage = qrCodeImageView.image else { return }
let viewController = owner.qrExpandViewControllerFactory(QRCodeImage, ticketName)
viewController.modalPresentationStyle = .fullScreen
owner.present(viewController, animated: true)
}
.disposed(by: self.disposeBag)
-
여기서 잠깐 disposable과 disposeBag에대해서 간단하게 확인을 해보자.
-
우리가 생성한 subscription의 disposable을 disposeBag에 넣는 메소드는 아래와 같이 구현이 되어있다.
-
기본적으로 자신의 bag에 disposable을 넣는 것이다.
extension Disposable {
/// Adds `self` to `bag`
///
/// - parameter bag: `DisposeBag` to add `self` to.
public func disposed(by bag: DisposeBag) {
bag.insert(self)
}
}
-
여기서 disposable은 아래와 같이 구현이 되어있다.
-
쉽게 말해서 우리가 흔히하는 Disposable.disposed(by: self.disposeBag)는 해당 disposable을 disposables라는 disposable을 담는 배열에 append하는 것이다. 여기서 중요한 부분은 deinit 부분이다. 만약 disposeBag가 deinit되면 dispose 메소드가 실행이되고, 이는 자신의 disposables에 들어가 있는 disposable들을 싹 다 비워버리는 것이다. 이 과정은 disposable들 즉, subscription들을 끊어버리는 과정이라고 생각할 수 있다.
public final class DisposeBag: DisposeBase {
private var disposables = [Disposable]()
public func insert(_ disposable: Disposable) {
self._insert(disposable)?.dispose()
}
private func _insert(_ disposable: Disposable) -> Disposable? {
self.lock.performLocked {
if self.isDisposed {
return disposable
}
self.disposables.append(disposable)
return nil
}
}
/// This is internal on purpose, take a look at `CompositeDisposable` instead.
private func dispose() {
let oldDisposables = self._dispose()
for disposable in oldDisposables {
disposable.dispose()
}
}
private func _dispose() -> [Disposable] {
self.lock.performLocked {
let disposables = self.disposables
self.disposables.removeAll(keepingCapacity: false)
self.isDisposed = true
return disposables
}
}
deinit {
self.dispose()
}
}
-
그렇다면 우리는 맨 위에서 말했던 cell의 재사용 이슈에 따른 중복 바인딩 이슈를 어떻게 해결할 수 있을까?
-
먼저 기존의 self.disposeBag에 넣었던 subscription의 disposable을 cell의 disposeBag에 넣는다.
final class TicketListCollectionViewCell: UICollectionViewCell {
var disposeBag = DisposeBag()
}
//...//
qrCodeImageView.rx.tapGesture()
.when(.recognized)
.asDriver(onErrorDriveWith: .never())
.drive(with: self) { owner, _ in
guard let QRCodeImage = qrCodeImageView.image else { return }
let viewController = owner.qrExpandViewControllerFactory(QRCodeImage, ticketName)
viewController.modalPresentationStyle = .fullScreen
owner.present(viewController, animated: true)
}
.disposed(by: cell.disposeBag)
- 그리고 우리는 해당 cell의 disposeBag을 cell의 재사용될 때마다 새롭게 갈아끼워줘야된다. 아래와 같이 cell을 뽑았을 때 설정해줘도 되고, 그게 아니라 prepareForReuse를 활용할 수 있다.
self.datasource = UICollectionViewDiffableDataSource(
collectionView: self.collectionView,
cellProvider: { [weak self ] collectionView, indexPath, item in
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: TicketListCollectionViewCell.className,
for: indexPath
) as? TicketListCollectionViewCell else { return UICollectionViewCell() }
cell.disposeBag = DisposeBag()
cell.setData(with: item)
if item.ticketStatus == .notUsed {
self?.bindQRCodeExpandView(cell, with: item)
}
return cell
})
/// 아니면 cell의 prepareForReuse
override func prepareForReuse() {
super.prepareForReuse()
self.ticketNumberLabel.text = nil
self.ticketTypeLabel.text = nil
self.ticketInformationView.resetData()
self.disposeBag = DisposeBag()
}
-
이러한 과정을 통해서 맨 위에서 말했던 재사용 이슈에 따른 중복 바인딩 에러를 해결할 수 있었다.
-
앞으로 collection view나 table view를 활용할 때 재사용에 관한 고민을 더 철저히 하고(데이터가 많으면 많을수록) disposeBag와 같이 무의식적으로 작성하는 코드에대해서도 의식적으로 고민을 해야할 필요성을 느끼게 되었다.