Upstream에서 여러 데이터를 발산하는 ViewModel의 Properties를 ViewController에서 .bind(onNext:)
로 바인딩하여 UI를 업데이트하려고 할 때,
UICollectionView로 구성된 View가 예상과 다르게 깨지는 현상이 발생했다.
![]() | ![]() |
---|
좌측의 GIF가 View가 최종적으로 그려져야하는 결과이고,
우측의 GIF가 View가 잘못 그려진 결과다.
❗ 여기서 좌측결과는 Cell에 Data까지 주입한 결과이고, 우측은 Data 주입이 안된 결과이다.
private func bind() -> Void {
// NewReleases
self.spotifyViewModel.albums.newReleases
.observe(on: MainScheduler.instance)
.bind { [weak self] newReleasesResponse in
guard let _: HomeViewController = self else { return }
self?.sections.append(.newReleases(newReleases: newReleasesResponse))
self?.collectionView.reloadData()
}.disposed(by: self.bag)
// FeaturedPlaylists
self.spotifyViewModel.playlists.featuredPlaylist
.observe(on: MainScheduler.instance)
.bind { [weak self] featuredPlaylistsResponse in
guard let _: HomeViewController = self else { return }
self?.sections.append(.featuredPlaylists(playlists: featuredPlaylistsResponse))
self?.collectionView.reloadData()
}.disposed(by: self.bag)
// Recommendations
self.spotifyViewModel.tracks.recommendations
.observe(on: MainScheduler.instance)
.bind { [weak self] recommendationsResponse in
guard let _: HomeViewController = self else { return }
self?.sections.append(.recommendations(tracks: recommendationsResponse))
self?.collectionView.reloadData()
}.disposed(by: self.bag)
}
먼저 기존의 작성한 로직이며, Data를 방출하는 Publisher를 구독하는 private func bind()
이다.
(사실 정확한 이유는 잘 모르겠다...)
문제를 해결하기 위해 ViewModel의 Observable을 하나로 통합하는 Rx의 .combineLatest()
메서드를 이용하여
(즉, Publisher가 방출하는 시퀀스들과 Upstream을 합쳐)
API 호출에 대한 이벤트 발생으로 이벤트 값을 최신값으로 조합하여 그 때, UI를 업데이트 하도록 변경하였다.
private func bind() -> Void {
// 각 ViewModel의 Property를 관찰하는 Observable 선언 및 초기화
let newReleasesObservable: BehaviorSubject<NewReleasesResponse?> = self.spotifyViewModel.albums.newReleases
let featuredPlaylistsObservable: BehaviorSubject<FeaturedPlayListsResponse?> = self.spotifyViewModel.playlists.featuredPlaylist
let recommendationsObservable: BehaviorSubject<RecommendationsResponse?> = self.spotifyViewModel.tracks.recommendations
// Observables를 결합
Observable.combineLatest(newReleasesObservable, featuredPlaylistsObservable, recommendationsObservable)
.observe(on: MainScheduler.instance)
.bind { [weak self] (newReleasesResponse, featuredPlaylistsResponse, recommendationsResponse) in
guard newReleasesResponse != nil, featuredPlaylistsResponse != nil, recommendationsResponse != nil else { return }
self?.sections.append(.newReleases(newReleases: newReleasesResponse))
self?.sections.append(.featuredPlaylists(playlists: featuredPlaylistsResponse))
self?.sections.append(.recommendations(tracks: recommendationsResponse))
self?.collectionView.reloadData()
}.disposed(by: self.bag)
}
위 코드를 조금 더 가독성 있게 SOLID의 SRP (= 단일 책임 원칙) 에 의거하여
private func bind() -> Void {
/// 각 ViewModel의 Property를 관찰하는 Observable 선언 및 초기화
let newReleasesObservable: BehaviorSubject<NewReleasesResponse?> = self.spotifyViewModel.albums.newReleases
let featuredPlaylistsObservable: BehaviorSubject<FeaturedPlayListsResponse?> = self.spotifyViewModel.playlists.featuredPlaylist
let recommendationsObservable: BehaviorSubject<RecommendationsResponse?> = self.spotifyViewModel.tracks.recommendations
/// Observables를 결합
let combinedObservable = Observable.combineLatest(newReleasesObservable, featuredPlaylistsObservable, recommendationsObservable) // Data Stream을 하나로 통합 -> Data의 수신 시점이 다른 문제를 해결할수 있음!
self.updateSectionsWhenDataArrives(combinedObservable: combinedObservable)
}
/// SOLID의 '단일 책임 원칙 (= SRP)'에 의거하여 메서드를 분리
private func updateSectionsWhenDataArrives(combinedObservable: Observable<(NewReleasesResponse?, FeaturedPlayListsResponse?, RecommendationsResponse?)>) -> Void {
combinedObservable
.observe(on: MainScheduler.instance)
.bind { [weak self] (newReleasesResponse, featuredPlaylistsResponse, recommendationsResponse) in
// 모든 Observable에서 데이터가 도착했으면 아래 블록 실행
guard newReleasesResponse != nil, featuredPlaylistsResponse != nil, recommendationsResponse != nil else { return }
self?.sections.append(.newReleases(newReleases: newReleasesResponse))
self?.sections.append(.featuredPlaylists(playlists: featuredPlaylistsResponse))
self?.sections.append(.recommendations(tracks: recommendationsResponse))
self?.collectionView.reloadData()
}.disposed(by: self.bag)
}
위와 같이 리팩터링을 할 수도 있겠다.
이 private func bind()
를 다시 호출하면
Cell에 Data 주입은 안했지만, View를 그리고자하는 최종 결과와 똑같은 결과를 얻었다!