RxDataSource
RxDataSource를 활용해 CollectionView를 사용하는 방법을 정리해보려고 한다.
RxDataSource가 필요한 이유
일반적인 상황에선 DataSource를 사용해 데이터를 표현하지만 Rx를 사용할 경우 아래와 메서드를 활용한다.
collectionView.rx.items(cellIdentifier: )
collectionView.rx.items(source: )
collectionView.rx.items(dataSource: RxCollectionViewDataSourceType & UICollectionViewDataSource)
이런 메서드를 통해 아래와 같은 방법으로 RxCocoa를 활용해 데이터를 표현한다.
let data = Observable.just(["first", "second", "third"])
data.bind(to: collectionView.rx.items(cellIdentifier: "Cell", cellType: UICollectionViewCell.self)) { index, model, cell in
cell.label.text = model
}
RxCocoa를 활용해도 TableView와 CollectionView를 구현해낼 수 있다.
하지만 RxCocoa만을 활용할 땐 제한이 존재한다.
1. 여러 섹션을 사용하기 힘들다.
2. 애니메이션을 구현하기 힘들다.
그래서 RxDataSource는 새로운 메서드와 프로토콜, 클래스를 제공한다.
rx.items(dataSource:) // dataSource를 input으로 받는다
SectionModelType // 여러 섹션에 대응할 수 있는 SectionModelType을 제공한다
Section and Animation // Section과 Animation을 적용 가능한 DataSource 클래스를 제공한다.
적용 과정
1. Data Model을 생성한다.
2. Section을 정의하기 위해 SectionModelType을 채택하는 구조체를 생성한다.
3. Section구조체 타입을 인자로 할당하는 DataSource를 정의한다.
4. rx.items(dataSource: ) 에 바인딩 한다.
따라해봅시다.
1. Data Model을 생성한다.
내가 만들고 있는 앱은 유저들이 짧은 동영상을 공유하는 앱이다. 그래서 Reels 구조체를 정의한다. 내가 보여줄 앱은 Reels 앱이기 때문에 Reels(동영상 정보)구조체가 Data Model이 될 것이다.
struct Reels: Codable {
let id: String
let title: String
let videoDescription: String
let githubUrl: String
let blogUrl: String
let hearts: Int
let date: Int
let uid: String?
let videoURL: String?
let thumbnailURL: String?
}
2. Section을 정의하기 위해 SectionModelType을 채택하는 구조체를 생성한다.
자.. RxDataSource의 장점중 하나는 여러 섹션에 대응할 수 있다는 장점이였다. 그럼 각 섹션은 헤더와 데이터를 가지고 있을 것이다.
섹션의 헤더를 정의해본다.
struct Header {
var profileImageURLString: String
var userName: String
var introduce: String
var githubURL: String
var blogURL: String
var postCount: String
var followerCount: String
var followingCount: String
var isMyProfile: Bool
var buttonType: ButtonEnableType
}
내가 만들 섹션의 헤더에는 유저의 정보가 들어가야한다. 사실 1개의 섹션만을 사용하며 헤더에는 유저의 정보가 들어갈 것이다. 그리고, 섹션 헤더의 Items에는 유저가 올린 Reels들의 영상정보가 보여질 것이다.
이 헤더와 Item을 담을 구조체를 정의한다. 여기에서 나오는 Item은 아래에서 나온다.
struct SectionOfReelsPost {
var header: Header
var items: [Item]
}
이후 확장에서 위에서 말한 SectionModelType을 채택해준다.
바로 이곳에서 typealias로 Item의 타입을 정해준다.
extension SectionOfReelsPost: SectionModelType {
typealias Item = Reels
init(original: SectionOfReelsPost, items: [Reels]) {
self = original
self.items = items
}
}
이렇게 생성된 SectionOfReelsPost는 설정하는 방법에 따라 여러 섹션을 보여줄수도, 아닐수도 있다.
3. Section구조체 타입을 인자로 할당하는 DataSource를 정의한다.
private lazy var dataSource = RxCollectionViewSectionedReloadDataSource<SectionOfReelsPost>(
configureCell: { (dataSource, collectionView, indexPath, item) in
guard let cell = collectionView.dequeueReusableCell(
withReuseIdentifier: ReelsCollectionCell.identifier,
for: indexPath
) as? ReelsCollectionCell else { return UICollectionViewCell() }
cell.bind(reels: item)
return cell
}, configureSupplementaryView: { [weak self] dataSource, collectionView, kind, indexPath in
guard let self = self,
let header = collectionView.dequeueReusableSupplementaryView(
ofKind: UICollectionView.elementKindSectionHeader,
withReuseIdentifier: ProfileHeaderView.identifier,
for: indexPath
) as? ProfileHeaderView else { return UICollectionReusableView() }
header.configure(header: dataSource.sectionModels[indexPath.section].header)
header.blogImageViewTap
.bind(to: self.blogImageViewTapSubject)
.disposed(by: disposeBag)
header.githubImageViewTap
.bind(to: self.githubImageViewTapSubject)
.disposed(by: disposeBag)
header.followButtonTap
.bind(to: self.followButtonTapSubject)
.disposed(by: disposeBag)
header.editButtonTap
.bind(to: self.editButtonTapSubject)
.disposed(by: disposeBag)
header.settingButtonTap
.bind(to: self.settingButtonTapSubject)
.disposed(by: disposeBag)
header.unfollowButtonTap
.bind(to: self.unfollowButtonTapSubject)
.disposed(by: disposeBag)
return header
})
여기서 보면 각 Cell과 header에 대한 설정을 하고있다. 참고로 header에서 tap을 self.의 Subject로 bind하고있는데, 이는 header에서 일어난 tap Event를 ViewModel로 전달히기위한 과정이니 별 내용은 아니다.
또한 여기서 생성한 DataSourceClass는 애니메이션을 사용 하는지, 안하는지에 따라 나뉜다.
- RxTableViewSectionedReloadDataSource, RxTableViewSectionedAnimatedDataSource
- RxCollectionViewSectionedReloadDataSource, RxCollectionViewSectionedAnimatedDataSource
현재 여기선 Animation을 사용하고 있지 않기 때문에 ReloadDataSource를 사용한다.
각 DataSource는 여러가지를 활용할 수 있으니 참고해보면 좋을듯하다.
4. rx.items(dataSource: ) 에 바인딩 한다.
override func bind() {
let input = ProfileViewModel.Input(
viewWillAppear: rx.viewWillAppear.map { _ in () }.asObservable(),
viewDidLoad: rx.viewDidLoad.map { _ in () }.asObservable(),
blogImageViewTap: blogImageViewTapSubject,
githubImageViewTap: githubImageViewTapSubject,
followButtonTap: followButtonTapSubject,
unfollowBUttonTap: unfollowButtonTapSubject,
editButtonTap: editButtonTapSubject,
settingButtonTap: settingButtonTapSubject,
backButtonTap: doneButton.rx.tap
.throttle(.seconds(1), scheduler: MainScheduler.asyncInstance)
)
let output = viewModel.transform(input: input)
// bindCollectionView
output.collectionViewDataSource
.drive(collectionView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
}
현재 MVVM의 In, Out 패턴을 사용하고 있기 때문에 Input과 OutPut을 을 정의해두고 transform을 활용해 OutPut을 생성해낸다.
이를 Output 에서 Driver로 return하기때문에 drive를 해준다.
output.collectionViewDataSource
.drive(collectionView.rx.items(dataSource: dataSource))
.disposed(by: disposeBag)
별거 없는 내용이지만 RxSwift의 이론만을 학습하고 실제고 프로젝트에 적용하기는 정말 너무너무너무 어렵다.
참고로 ViewModel에서 어떻게 Output을 만들어 내고 있냐면..
private func transformCollectionViewDataSource(input: ProfileViewModel.Input) {
Observable.combineLatest(
currentUser.compactMap { $0 }.asObservable(),
follower,
following,
reels,
type,
buttonType
)
.map { user, follower, following, reels, type, buttonType -> [SectionOfReelsPost] in
let header = Header(
profileImageURLString: user.profileImageURLString,
userName: user.nickName,
introduce: "<" + " \(user.introduce) " + "/>",
githubURL: user.githubURL,
blogURL: user.blogURL,
postCount: "\(reels.count)",
followerCount: "\(follower.count)",
followingCount: "\(following.count)",
isMyProfile: type == .current ? true : false,
buttonType: buttonType
)
return [
SectionOfReelsPost(
header: header,
items: reels
)
]
}
.withUnretained(self)
.subscribe(onNext: { viewModel, result in
viewModel.collectionViewDataSource.onNext(result)
})
.disposed(by: disposeBag)
}
각각 combineLatest내부에서 사용되는 것들은 Subject들이고 이들의 이벤트를 모아서 하나의 SectionOfReelsPost로 만들어서 return하게 된다.
이후 viewModel에 있는 "CollectionViewDataSource" Subject에 onNext하게되고, 이를 OutPut으로 사용하게 된다.
다양한 인풋과, 유즈케이스를 통한 Remote, Local에 존재하는 Data를 활용해 SectionOfReelsPost를 만들어내고, 이를 컬렉션뷰에 바인딩하는 방식
이렇게 처음 RxDataSource로 만들어본 CollectionView는 이렇게 생겼다!
모두 구현은 완료했지만 Dummy Data가 아직 부족해서 없어보이긴 한다..
아직 부족한점이 많아 틀린 정보가 있을 수도 있습니다. 틀린 정보나 부족한 점이 있다면 언제나 댓글은 환영입니다.