[RxSwift] - 데이터 스트림 통합으로 UICollectionView UI 문제 해결

Ben·2023년 9월 20일
0

iOS

목록 보기
9/9

Trouble-shooting 💥

문제

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() 인데,
외부 API의 Data 수신 시점이 다르기 때문에 UI가 예상대로 그려지지 않는 것이었다.
(↪ 여기서 UI Component에 대한 로직들은 따로 올리진 않겠다.)


해결

문제를 해결하기 위해 ViewModel의 Properties에서 데이터를 수신하는 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를 그리고자하는 최종 결과와 똑같은 결과를 얻었다!

profile
 iOS Developer

0개의 댓글