- Published on
๐ฑ iOS - RxDataSources์ reloadData
- Authors
- Name
- ์ด์ฐฝ์ค
๐ต DataSource๋ฅผ reload ํ๋ ๊ณผ์ ์์ ๋ฌธ์ ๋ฐ์
MyPage๋ฅผ ํธ์งํ๋ View๋ฅผ ๊ตฌํํ๋ ์ค ํ๋์ Section ๊ฐ์ด ์ ๋ฐ์ดํธ ๋๋ฉด ๋ค๋ฅธ Section์ ๊ฐ์ด ์ด๊ธฐ๊ฐ์ผ๋ก ๋์๊ฐ๋ ํ์์ ๋ง์ฃผํ์ต๋๋ค.
ํด๋น View๋ ํ๋์ UICollectionView
๋ก ๊ตฌ์ฑ๋์์ผ๋ฉฐ ์ต์๋จ์ ํ๋กํ/๋ฐฐ๊ฒฝ ์ด๋ฏธ์ง ๋ณ๊ฒฝ ๋ถ๋ถ์ Header, ์ด๋ฆ/ID/์ทจํฅ ๋ถ๋ถ์ Section์ผ๋ก ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
private lazy var dataSource: EditMyPageDataSource = EditMyPageDataSource(
configureCell: { [weak self] _, collectionView, indexPath, item in
switch item {
case let .name(name, placeholder):
let cell = collectionView.dequeueReusableCell(for: indexPath) as FavorTextFieldCell
cell.bind(placeholder: placeholder)
cell.bind(text: name)
return cell
case let .id(name, placeholder):
let cell = collectionView.dequeueReusableCell(for: indexPath) as FavorTextFieldCell
cell.bind(placeholder: placeholder)
cell.bind(text: name)
return cell
case let .favor(isSelected, favor):
let cell = collectionView.dequeueReusableCell(for: indexPath) as EditMyPagePreferenceCell
cell.isButtonSelected = isSelected
cell.favor = favor
return cell
}
}, configureSupplementaryView: { dataSource, collectionView, kind, indexPath in
switch kind {
case EditMyPageCollectionHeaderView.reuseIdentifier:
let header = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
for: indexPath
) as EditMyPageCollectionHeaderView
return header
case UICollectionView.elementKindSectionHeader:
let header = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
for: indexPath
) as FavorSectionHeaderView
let headerTitle = dataSource[indexPath.section].header
header.updateTitle(headerTitle)
return header
case UICollectionView.elementKindSectionFooter:
let footer = collectionView.dequeueReusableSupplementaryView(
ofKind: kind,
for: indexPath
) as FavorSectionFooterView
footer.footerDescription = dataSource[indexPath.section].footer
return footer
default:
return UICollectionReusableView()
}
}
)
DataSource๋ฅผ ๋ฐ์ธ๋ฉํ๋ ๊ณผ์ ์ Rx์์ ํธํ์ฑ์ ๊ณ ๋ คํ์ฌ RxDataSources ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค. ์ถ๊ฐ์ ์ผ๋ก cell์ dequeueํ๊ณ identifier๋ฅผ ์ ์ํ๋ ๊ณผ์ ์ ํธ๋ฆฌํ๊ฒ ๊ตฌํํ๊ธฐ ์ํด Reusable ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฌ์ฉํ๊ณ ์์ต๋๋ค.
โ ์์ธ ํ์
๊ฐ Cell์ rx๋ฅผ ์ฌ์ฉํ์ฌ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ธ๋ฉํ๋ ๊ณผ์ ์์์ ๋ฌธ์ , Reactor์ transform
์์ newState
๊ฐ ์๋กญ๊ฒ ์์ฑ๋๋ ๋ฌธ์ ๋ฑ์ ๊ณ ๋ คํ์ง๋ง ๊ฒฐ๊ตญ์๋ ๋ชจ๋ ์๋์์ต๋๋ค.
์์ธ์ dataSource
์ print
๋ฌธ์ ๋ฃ์ ํ์ ๋ฐ๊ฒฌํ ์ ์์์ต๋๋ค.
private lazy var dataSource: EditMyPageDataSource = EditMyPageDataSource(
configureCell: { [weak self] _, collectionView, indexPath, item in
switch item {
case let .name(name, placeholder):
let cell = collectionView.dequeueReusableCell(for: indexPath) as FavorTextFieldCell
cell.bind(placeholder: placeholder)
cell.bind(text: name)
print(cell) // ๐๏ธ ์ฌ๊ธฐ
return cell
case let .id(name, placeholder):
let cell = collectionView.dequeueReusableCell(for: indexPath) as FavorTextFieldCell
cell.bind(placeholder: placeholder)
cell.bind(text: name)
print(cell) // ๐๏ธ ์ฌ๊ธฐ
return cell
case let .favor(isSelected, favor):
let cell = collectionView.dequeueReusableCell(for: indexPath) as EditMyPagePreferenceCell
cell.isButtonSelected = isSelected
cell.favor = favor
return cell
}
๊ฐ Section์ ํ๋์ Item๋ง ํฌํจ๋์ด ์์ผ๋ฏ๋ก ์ทจํฅ Section์ด ๋ณ๊ฒฝ๋ ๋๋ง๋ค ๋ชจ๋ Section์ dataSource
๊ฐ ๋ค์ ์ ์๋๊ณ ์์๋ ๊ฒ์
๋๋ค.
๋ชจ๋ Section์ ๋ฐ์ดํฐ๊ฐ reload๋๊ณ ์๋ค๋ ์์ฌ์ ํ๊ณ RxDataSources ๋ด๋ถ ์ฝ๋๋ฅผ ํ์ ํด๋ณด๊ธฐ๋ก ํ์ต๋๋ค.
โ ์์ธ ๋ฐ๊ฒฌ
๋ฌธ์ ๋ RxDataSources๊ฐ ๋ง์์ต๋๋ค.
์ผ๋จ ์ ํฌ๊ฐ ์ฌ์ฉํ๊ณ ์๋ RxCollectionViewSectionedReloadDataSource
์ ๊ฒฝ์ฐ๋ ๋ค์๊ณผ ๊ฐ์ด ๊ตฌ์ฑ๋์ด ์์ต๋๋ค.
open class RxCollectionViewSectionedReloadDataSource<Section: SectionModelType>
: CollectionViewSectionedDataSource<Section>
, RxCollectionViewDataSourceType {
public typealias Element = [Section]
open func collectionView(_ collectionView: UICollectionView, observedEvent: Event<Element>) {
Binder(self) { dataSource, element in
#if DEBUG
dataSource._dataSourceBound = true
#endif
dataSource.setSections(element)
collectionView.reloadData() // ๐ ์ฌ๊ธฐ!
collectionView.collectionViewLayout.invalidateLayout()
}.on(observedEvent)
}
}
UIKit์ UICollectionView
์ ๊ฐ์ reloadํ๋ ๋ฐฉ๋ฒ์ผ๋ก ํฌ๊ฒ
- ๋ชจ๋ ๊ฐ์ reloadํ๋
reloadData()
- ํน์ Section๋ง์ reloadํ๋
reloadSections()
- ๋ง์ง๋ง์ผ๋ก ํน์ Item๋ง์ reloadํ๋
reloadItem(at:)
์ ์ธ๊ฐ์ง ์ข
๋ฅ API๋ฅผ ์ ๊ณตํ๊ณ ์์ต๋๋ค. ํ์ง๋ง RxDataSources์ ๊ฒฝ์ฐ, ๋ฌด์กฐ๊ฑด reloadData()
๋ฅผ ํธ์ถํ์ฌ ๋ชจ๋ ๊ฐ์ reloadํ๊ณ ์๋ ์ ์ ๋ณผ ์ ์์ต๋๋ค.
ChatGPT์๊ฒ ๋ฌผ์ด๋ณด๋ RxDataSources๋ ๊ท๋ชจ๊ฐ ์์ ๋ฐ์ดํฐ๋ฅผ ๋ค๋ฃฐ ๋ ์ฌ์ฉ๋๋ ๊ฒ์ ๊ณ ๋ คํ์ฌ ์ค๊ณ๋์๊ณ , ๋ชจ๋ ๊ฐ์ reloadํ๋ ๊ฒ์ด ๋ค๋ฅธ trade-off์ ๋น๊ตํ์ ๋ ํจ์จ์ ์ผ๋ก ํ๋จํ๋ค๊ณ ํฉ๋๋ค.
์ ํํ ์ ๋ณด์ธ์ง ๊ฐ์ธ์ ์ผ๋ก ์ถ์ฒ๋ฅผ ์ฐพ์๋ณด๋ ค ํ์ง๋ง ์ฐพ์๋ณผ ์๋ ์์์ต๋๋ค..
์ ๋ณด ๋ฐ์ต๋๋ค.. ๐
.name
๊ณผ .id
์น์
์ ๊ฒฝ์ฐ์๋ View๊ฐ ๋ก๋๋์ ๋ ๋จ ํ ๋ฒ๋ง ๋ฐ์ดํฐ๊ฐ ๋ก๋๋๊ธฐ ๋๋ฌธ์ ๋ชจ๋ Section์ด reload๋๋ ๊ฒฝ์ฐ ์ด๊ธฐ๊ฐ์ผ๋ก ๋์๊ฐ๋ ๊ฒ์
๋๋ค.
ํน์๋ RxCollectionViewSectionedAnimatedDataSource
๋ ๋ณ๊ฒฝ๋๋ Section์ ๊ณจ๋ผ๋ด์ ๋ถ๋ถ์ ์ผ๋ก reload๊ฐ ๋๋ ์ถ์์ง๋ง.. ๋ง์ฐฌ๊ฐ์ง๋ก ์ ๋๋ฉ์ด์
์ฒ๋ฆฌ๋ง ๋ถ๋ถ์ ์ผ๋ก ์ฒ๋ฆฌ๋๊ณ reload๋ ์ ์ฒด์ ์ผ๋ก ๋๊ณ ์์์ต๋๋ค..
โผ๏ธ ํด๊ฒฐ
์ด๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด์๋ ๋ ๊ฐ์ง ํด๊ฒฐ๋ฒ์ด ์์ต๋๋ค.
- Custom DataSource ํ์ ์ ๋ง๋ค์ด ์ฌ์ฉํ๋ค.
- RxDataSources๋ฅผ ์ฌ์ฉํ์ง ์๋๋ค.
Custom DataSource ์ฌ์ฉ
class FavorDataSource<Section: SectionModelType>: RxCollectionViewSectionedReloadDataSource<Section> {
override func collectionView(_ collectionView: UICollectionView, observedEvent: Event<[Section]>) {
Binder(self) { dataSource, element in
dataSource.setSections(element)
collectionView.reloadSections([2], animationStyle: .none) // ๐ ํ
์คํธ๋ฅผ ์ํด ๊ณ ์ ์ ์ผ๋ก 2๋ฒ์งธ ์น์
๋ง reload๋๋๋ก
collectionView.collectionViewLayout.invalidateLayout()
}.on(observedEvent)
}
}
์ฒซ ๋ฒ์งธ ๋ฐฉ๋ฒ์ ๋๋ค.
์์ ๊ฐ์ด Custom DataSource๋ฅผ ๋ง๋ค์ด ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋๋ค.
reloadData()
๋์ reloadSections
๋ reloadItem(at:)
์ ์ฌ์ฉํ๋ DataSource๋ฅผ ๋ฐ๋ก ๋ง๋ค์ด ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์
๋๋ค.
ํ ์คํธ๋ฅผ ์ํด ์ฐ์ 2๋ฒ์งธ Section์ ๊ฐ๋ค๋ง reload ๋๋๋ก ๊ตฌํํด๋ณด์์ต๋๋ค.
Section์ด ๊น๋นก์ด๊ธด ํ์ง๋ง ์ผ๋จ ์ํ๋๋๋ก ๋์์ ํ๋๊ตฐ์..
๋จ์ ์ Section์ ํํ์ ๋ฐ๋ผ DataSource๋ฅผ View์ ๋ง๊ฒ ๊ฐ๊ฐ ๋ง๋ค์ด์ ์ฌ์ฉํด์ผ ํ ๊ฐ๋ฅ์ฑ์ด ์๋ค๋ ์ ์ด์์ต๋๋ค.
โ์ธ์ ์ด๋ค Section์ reloadํด์ผํ๋๊ฐ?โ๋ฅผ Genericํ๊ฒ ๊ณ์ฐํด์ฃผ์ด์ผ ํ๋ค๋ ์ ์ ์ผ๋ฐ์ ์ธ ๊ฒฝ์ฐ์์๋ ๊ฝค๋ ๋ณต์กํ ๋ก์ง์ ํ์๋ก ํ๊ณ , ์ถํ ๊ณผ์ ์์ ์คํ๊ฒํฐ ์ฝ๋๊ฐ ๋ ๊ฒ์ด๋ผ๋ ์๊ฐ์ด ๋ค์ด ๋ถ์ ์ ํ๋ค๊ณ ํ๋จํ์์ต๋๋ค.
RxDataSources ๋์ DiffableDataSources ์ฌ์ฉ
์ฌ์ค ๊ฐ์ฅ ์ข์ ๋ฐฉ๋ฒ์ Native ๊ธฐ๋ฅ์ ์ฌ์ฉํ๋ ๊ฒ์ด๊ฒ ์ฃ ?
์ ๋ DiffableDataSources์ ๋ํ ์ถฉ๋ถํ ๊ณต๋ถ๋ฅผ ํ ๋ค ์ ํฌ ํ๋ก์ ํธ์ RxData Source๋ฅผ DiffableDataSources๋ก ๋์ฒดํด๋ ๋ฌธ์ ๊ฐ ์๊ฒ ๋ค๋ ํ๋จ์ ํ๊ณ RxDataSources๋ฅผ ๊ฑท์ด๋ด๊ธฐ๋ก ๊ฒฐ์ ํ์์ต๋๋ค.
Rx๋ฅผ ์ฌ์ฉํ๋ฉด์๋ DiffableDataSources๋ฅผ ์ฌ์ฉํ๋ ๋ฐ์๋ ์๋ฌด ๋ฌธ์ ๊ฐ ์์ ๊ฒ ๊ฐ๋๋ผ๊ณ ์..
bind(to:)
๋ฅผ ์ฌ์ฉํ์ฌ ๊ฐํธํ๊ฒ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์ธ๋ฉํ ์ ์๋ค๋ผ๋ ์ฅ์ ๋ง ํฌ๊ธฐํ๋ค๋ฉด RxDataSources๋ฅผ ๊ฑท์ด๋ด๋ ๋ฐ์๋ ์๋ฌด ๋ฌธ์ ๊ฐ ์์์ต๋๋ค.
์ฌ์ง์ด API๋ฅผ ์ฌ์ฉํ๋ ํํ๋ ์๋นํ ์ ์ฌํด์ ๋ฌ๋ ํ๋ค๋ ๋ฎ์ ํธ์ ์ํ์ต๋๋ค.
ํ์ง๋ง ์ ์ํฉ์์์ DiffableDataSources๋ฅผ ์ฌ์ฉํ๋ ๊ฐ์ฅ ํฐ ์ด์ ์ ์ ๊ฐ ์ํ๋ ์์ ์ ์ ๊ฐ ์ํ๋ Section์ด๋ Item๋ง์ ์ ๋ฐ์ดํธ ํด์ค ์ ์๋ค๋ ์ ์ด์์ต๋๋ค.
์ ์ด์ ๋ฆฌํฉํ ๋ง์ ํด๋ด ์๋ค..! ๐ฅ
1. Section ์ ์
์ฐ์ ๋ ๊ฐ์ง ํ๋กํ ์ฝ์ ์ ์ํด์ฃผ์์ต๋๋ค.
public protocol SectionModelType: Hashable { }
public protocol SectionModelItem: Hashable { }
SectionIdentifier๋ก ์ฌ์ฉ๋ SectionModelType
๊ณผ ItemIdentifier๋ก ์ฌ์ฉ๋ SectionModelItem
ํ๋กํ ์ฝ์
๋๋ค.
์ด ๋๊ฐ์ง ํ๋กํ ์ฝ์ ์๋ฌด ๊ธฐ๋ฅ๋ ํ์ง ์๊ณ ์ค์ง Hashable
์ ์ฑํํด์ฃผ๊ธฐ๋ง ํ์ต๋๋ค. (DiffableDataSources์์ ์ฌ์ฉ๋๋ ค๋ฉด Hashableํด์ผํฉ๋๋ค!)
์ถํ์ ํ์ํ๋ค๋ฉด ๊ธฐ๋ฅ์ ์ถ๊ฐํด์ฃผ๊ฒ ์ง๋ง ์์ง์ Generic์์์ ํ์ ์ ํ ์ฉ๋๋ก๋ง ์ฌ์ฉํ๊ณ ์์ต๋๋ค.
enum ProfileSectionItem: SectionModelItem {
case profileSetupHelper(ProfileSetupHelperCellReactor)
case preferences(ProfilePreferenceCellReactor)
case anniversaries(ProfileAnniversaryCellReactor)
case memo
case friends(ProfileFriendCellReactor)
}
enum ProfileSection: SectionModelType {
case profileSetupHelper
case preferences
case anniversaries
case memo
case friends
}
๊ทธ๋ฆฌ๊ณ ๊ทธ ๋๊ฐ์ง ํ๋กํ ์ฝ์ ์ฑํํ ์์์ ๋๋ค.
SectionModelType
์ ์ฑํํ Section๋ค๊ณผ SectionModelItem
์ ์ฑํํ Item์ ์ ์ํด์ค๋๋ค.
2. Reactor State ๋ณ๊ฒฝ
var sections: [ProfileSection] = []
var items: [[ProfileSectionItem]] = []
var profileSetupHelperItems: [ProfileSectionItem] = []
var preferencesItems: [ProfileSectionItem] = []
var anniversaryItems: [ProfileSectionItem] = []
var friendItems: [ProfileSectionItem] = []
์ ๋ ๊ฐ Section์ ๋์๋๋ Item๋ค์ ๋ฐ๋ก ๊ด๋ฆฌํด์ฃผ๊ณ ์์ต๋๋ค.
์ด๋ ๊ฒ ํ๋ ํธ์ด ๋ฐ์ดํฐ๋ค์ ์์ ํด์ฃผ๋ ๋ก์ง์ ์งค ๋ ํธํ๋๋ผ๊ตฌ์..
func transform(state: Observable<State>) -> Observable<State> {
return state.map { state in
var newState = state
var newSections: [ProfileSection] = []
var newItems: [[ProfileSectionItem]] = []
var profileSetupHelperItems: [ProfileSectionItem] = []
// ์ทจํฅ
if !state.preferencesItems.isEmpty {
newSections.append(.preferences)
newItems.append(state.preferencesItems)
} else {
profileSetupHelperItems.append(.profileSetupHelper(ProfileSetupHelperCellReactor(.preference)))
}
// ๊ธฐ๋
์ผ
if !state.anniversaryItems.isEmpty {
newSections.append(.anniversaries)
newItems.append(state.anniversaryItems)
} else {
profileSetupHelperItems.append(.profileSetupHelper(ProfileSetupHelperCellReactor(.anniversary)))
}
// ์น๊ตฌ
newSections.append(.friends)
newItems.append(state.friendItems)
// ์ ํ๋กํ
if !profileSetupHelperItems.isEmpty {
newSections.insert(.profileSetupHelper, at: .zero)
newItems.insert(profileSetupHelperItems, at: .zero)
}
newState.sections = newSections
newState.items = newItems
return newState
}
}
๊ฐ Item๋ค์ ๋ํ ๋ฐ์ดํฐ๋ค์ transform(state:)
์์ ํตํฉํด์ฃผ๋ ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌํด์ฃผ๊ณ ์์ต๋๋ค.
3. DataSource์ ๋ฐ์ธ๋ฉ
reactor.state.map { (sections: $0.sections, items: $0.items) }
.asDriver(onErrorRecover: { _ in return .empty()})
.drive(with: self, onNext: { owner, sectionData in
var snapshot: NSDiffableDataSourceSnapshot<ProfileSection, ProfileSectionItem> = .init()
snapshot.appendSections(sectionData.sections)
sectionData.items.enumerated().forEach { idx, items in
snapshot.appendItems(items, toSection: sectionData.sections[idx])
}
owner.dataSource.apply(snapshot, animatingDifferences: false)
})
.disposed(by: self.disposeBag)
sections์ items ๋ชจ๋ Hashableํ๊ธฐ ๋๋ฌธ์ ์์ ๊ฐ์ด appendSections
, appendItems
๋ฑ์ DiffableDataSources์ API๋ฅผ ์ฌ์ฉํ์ฌ dataSource
์ ๋ฐ์ธ๋ฉํด์ฃผ๋ฉด ๋์
๋๋ค!