이번에 콜렉션 뷰로 프로젝트를 하면서 삽질했던 기록을 남기려고 한다. 예상하지 못한 문제들이 연속으로 터고, 그때마다 디버깅을 해야하는 영역? 접근법도 달라서 정말 골치가 아팠다.
하지만 지나고 보니 배운 게 많다. 의미있는 삽질이었다. 게다가 앞으로도 콜렉션 뷰(Collection view)의 비동기 로딩은 분명 써먹을 일이 많을테니.
정리가 깔끔하진 않지만, 기록 삼아 남겨둔다.
CategorySectionViewModel
로 추상화했고, 이 데이터를 상위 뷰 모델에서 Array<CategorySectionViewModel>
로 갖고 있도록 만들었다. DispatchGroup
이다. enter()
와 leave()
를 사용한다.notify()
를 사용해서 완료 시 실행할 작업을 지정할 수 있다.func fetch() {
var tempStorage = [CategorySectionViewModel]()
let dispatchGroup = DispatchGroup()
for type in ProductType.allCases {
// DispatchGroup에 작업 시작을 알린다.
dispatchGroup.enter()
categoryManager.fetchCategory(of: type) { category in
let productCellVMs = category.product.compactMap { product in
// productCellVM을 만든다.
}
// productCellVM을 배열로 가진 CategorySectionViewModel을 만든다.
let categorySectionVM = CategorySectionViewModel(type: .main, productVM: productCellVMs)
// 임시 저장소에 추가한다.
tempStorage.append(categorySectionVM)
// DispatchGroup에 작업 완료를 알린다.
dispatchGroup.leave()
}
}
// 모든 작업이 끝나면 oberverable에 데이터를 업데이트한다.
// 즉, collection view reload를 실행시킨다.
dispatchGroup.notify(queue: .global(), work: DispatchWorkItem {
print("Every section data updated")
cellViewModels.value = temp
})
}
reloadData()
로 완전히 새로고침하는 수밖에 없는...func reloadSections(_ sections: IndexSet)
CategoryType.allCases.forEach({ type in
guard let categoryVM = viewModel.categoryVMs[type] else {return}
categoryVM.bind { _ in
DispatchQueue.main.async {
self.mainCollectionView.reloadSections(IndexSet(integer: type.index))
}
}
})
[UICollectionView] Performing reloadData as a fallback
Invalid update: invalid number of items in section 0.
The number of items contained in an existing section after the update (8)
must be equal to the number of items contained in that section before the update (5) (...)
(뭔 소리야..?)
reloadSection
이 실행되지 않고, fallback으로 reloadData
가 실행되고 있었다.Alamofire
를 선택해서 쓰고 있었다.CategoryType.allCases.forEach({ type in
guard let categoryVM = viewModel.categoryVMs[type] else {return}
categoryVM.bind { _ in
DispatchQueue.main.async {
self.mainCollectionView.reloadSections(IndexSet(integer: type.index))
}
}
})
(아까 나왔던 코드)
✅ Data Updated: side (main thread: true)
✅ Data Updated: main (main thread: true)
▶️ Start Reload Section: side (main thread: true)
2022-05-03 10:39:01.869036+0900 SideDishApp[98453:9502835] [UICollectionView] Performing reloadData as a fallback
— Invalid update: invalid number of items in section 0. The number of items contained in an existing section after the update (8) must be equal to the number of items contained in that section before the update (5), plus or minus the number of items inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of items moved into or out of that section (0 moved in, 0 moved out).
Collection view: <UICollectionView: 0x141039400; frame = (0 91; 390 719); clipsToBounds = YES; autoresize = RM+BM; gestureRecognizers = <NSArray: 0x600002d7f060>; layer = <CALayer: 0x600002398d60>; contentOffset: {0, 0}; contentSize: {390, 1138}; adjustedContentInset: {0, 0, 0, 0}; layout: <UICollectionViewCompositionalLayout: 0x14021abe0>; dataSource: <SideDishApp.MainViewController: 0x13d716b40>>
⏹ End Reload Section: side (main thread: true)
▶️ Start Reload Section: main (main thread: true)
⏹ End Reload Section: main (main thread: true)
✅ Data Updated: soup (main thread: true)
▶️ Start Reload Section: soup (main thread: true)
⏹ End Reload Section: soup (main thread: true)
side
데이터가 업데이트 되고난 후, 뷰의 reload
가 바로 일어나는 게 아니라, main
데이터가 업데이트되고 있다.side
데이터로 뷰를 업데이트하는 시점에 에러가 발생한다.reloadSection
이 제대로 실행되지 않았던 거였다!DispatchQueue.main.async
를 지워주었다.✅ Data Updated: soup (main thread: true)
▶️ Start Reload Section: soup (main thread: true)
⏹ End Reload Section: soup (main thread: true)
✅ Data Updated: side (main thread: true)
▶️ Start Reload Section: side (main thread: true)
⏹ End Reload Section: side (main thread: true)
✅ Data Updated: main (main thread: true)
▶️ Start Reload Section: main (main thread: true)
⏹ End Reload Section: main (main thread: true)
우와.. 결국 해결 하셨네요! 많이 배워갑니다 👍