요리조리 RxSwift 사용기 - 1 (Feat: CombineLatest)

Inwoo Hwang·2022년 5월 18일
0
post-thumbnail

요구사항

두 가지 cell타입에 대응하는 모델을 만들어야 하는 상황이었다:
1. 하나는 유저가 선택한 사진을 cell에 보여주는 모델
2. 두 번째는 사진 업로드 필요성을 유저에게 보여주는 모델

또한 이 모델 배열 속 요소가 총 10개여야 한다는 점:
- 기획서에서 요구하는 건 정확히 10개의 cell이었다.

생각의 흐름

업로드 된 이미지 모델 배열, 그리고 업로드 필요 모델 배열을 가지고
[cellType.uploaded], [cellType.needToUpload]

  • 업로드 된 배열의 요소를 모두 받은 뒤 나머지는 업로드 필요 배열의 요소로 채운다.
  • 하지만 배열의 크기는 10개로 제한되기 때문에 유저가 선택한 숫자에 맞춰서 업로드 필요요소의 숫자를 조절해야 한다.
  • 유저가 선택한 사진들로 먼저 cell을 채워야 한다. 나머지는 업로드 필요 모델로 채운다.

기존 코드

private func bindTotalUploadPhotos() {

// from연산자를 활용하여 uploadedPhotoRelay.value(배열), needToUploadRelay.value(배열) 각 배열의 요소를 연속적으로 방출한다.
        let uploadedPhotoObservable = Observable.from(uploadedPhotoRelay.value)
        let needToUploadPhotoObservable = Observable.from(needToUploadPhotoRelay.value)

        return Observable
            // 연속적으로 방출되는 Observable들의 원소들을 concat 연산자를 활용하여 방출되는 원소들의 순서를 지정한다.
            // 유저가 첨부한 사진부터 방출하도록 순서를 지정하는 작업
            .concat([uploadedPhotoObservable, needToUploadPhotoObservable])

            // 총 사진은 제한되어야 하기 때문에 take연산자를 활용하여 방출되는 갯수를 10개로 제한 한다.
            .take(maxPostedPhotoCountRelay.value)

            // 컬렉션뷰에 바인딩하기 위해서는 Sequence protocol을 채택해야 하기 때문에 방출한 이벤트들을 배열로 만들어주어야 한다.
            .toArray()
            .asObservable()

			   // 이렇게 만들어준 값을 totalUploadPhotoRelay에 바인딩 시켜 추후 collectionView에 바인딩 될 때 사용할 수 있게 준비시킨다.
            .bind(to: self.totalUploadPhotoRelay)
            .disposed(by: disposeBag)
    }

// MARK: - TODO: 유저가 사진을 업로드 하거나 사진을 삭제할 경우 해당 메서드를 호출 해야만 수정된 값이 컬렉션뷰에 적용된다. 이 메서드를 매번 호출하는 것이 아니라 요소에 변화가 있으면 자동적으로 변화를 컬렉션뷰에 적용하는 방법은 없을까.

Mark에 글을 남겨둔 것 처럼 해당 메서드는 유저가 매번 사진 선택/해지를 할 때 마다 매번 현재 상태를 확인하기 위해 메서드를 실행해야 한다는 불편함이 있다. 가령 uploadedPhotoList의 값을 수정한 뒤 다음 과 같이 맨 밑줄에 위 메서드를 다시 한 번 더 호출 해야 한다는 번거로움이 있었다. 물론 이는 내가 RxSwift에 대한 이해가 부족한 점이 가장 큰거 같다.
다시 호출해야 하는 이유는 내 생각에는 처음에 만드는 from 연산자를 통해 받는 uploadedPhotoRelay.valueneedToUploadPhotoRelay.value의 배열들의 요소를 연속적으로 모두 방출하게 되면 completed 이벤트를 호출하게 되니까 더 이상 새로운 값을 받지 못하는게 큰거 같다.


// ViewModel.swift
func updateUplodedPhotoList(_ photos: [UIImage]) {
        
        // MARK: - 업로드된 항목 이미지배열에 접근 한 뒤 새롭게 선택된 사진들로 배열을 업데이트 해준다.
        var existingPhotoList = self.uploadedPhotoRelay.value.compactMap { $0.image }
        existingPhotoList = photos
        
        // MARK: - collectionview에 필요한 이미지배열 형태로 매핑하는 작업
        let newUploadPhotoList = existingPhotoList.map { UploadImageModel(image: $0, cellType: .uploaded)}
        
        // MARK: - 새로운 값을 넣어준 뒤 다시 컬랙션뷰에 바인딩 시켜준다.
        self.uploadedPhotoRelay.accept(newUploadPhotoList)
        
		  // MARK: - 값이 변경 되었으니 다시 바인딩 작업을 해야 한다.
        self.bindTotalUploadPhotos()
    }

그럼 어떻게 해야 하나? 🤔

개선 한 부분

CombineLatest를 활용

The CombineLatest operator behaves in a similar way to Zip, but while Zip emits items only when each of the zipped source Observables have emitted a previously unzipped item, CombineLatest emits an item whenever any of the source Observables emits an item (so long as each of the source Observables has emitted at least one item).
ReactiveX - CombineLatest operator

그렇다 combineLatest 는 두 Observable 중 하나라도 새로운 값을 방출하게 되면 두 각 소스의 맨 마지막 값을 뽑아서 새로운 값을 방출하게 된다. 클로저가 각 Observable이 방출했던 최신 값을 받게 되는 것이다.
이를 활용해서 uploadPhotoRelay와 needToUploadPhotoRelay 값을 받게 되니 이제 별도의 메서드 호출이 필요 없어졌습니다. uploadPhotoRelay(유저가 사진을 선택/삭제시 값이 변경되는 Relay)에 값이 변경될 때마다 Observable.combineLatest 가 호출될테니 말이다.

var photoCollectionViewDataObservable: Observable<[UploadImageModel]> {
        return Observable
        	// MARK: - relay 중 하나라도 값이 업데이트 될 경우 최신 값들(업로드이미지모델 배열들)을 방출하게 된다.
            .combineLatest(uploadedPhotoRelay, needToUploadPhotoRelay)
            
            // 기존 로직이 들어가있는 Observable을 반환해야 하기 때문에 flatMap연산자를 활용하였다.
            .flatMap { uploadedImageModelArray, needToUploadImageModelArray -> Observable<[UploadImageModel]> in
                let uploadedObservable = Observable.from(uploadedImageModelArray)
                let needToUploadObservable = Observable.from(needToUploadImageModelArray)
                return Observable.concat([uploadedObservable, needToUploadObservable])
                    .take(self.maxPostedPhotoCountRelay.value)
                    .toArray()
                    .asObservable()
            }
    }

photoCollectionViewDataObservable은 collectionView가 구독해야하기 때문에 구독할 수 있는 Observable타입이어야 한다. 따라서 .flatMap 를 활용하여 방출된 이벤트들을 새로운 Observable로 바꿔서 해당 Observable을 반환하게한다. .flatMap 속에서 이루어지는 로직은 기존과 동일하다.

무튼 combineLatest를 활용하니 이제 BehaviorRelay의 값이 바뀌어도 매 번 이를 직접 반영해줄 필요가 없어졌다는 점이 큰 메리트였다.
이제 그냥 아래와 같이 photoCollectionViewDataObservable 이름을 가진 연산 프로퍼티를 컬렉션뷰가 구독하기만 하면 되기 때문이다 :)

viewModel.outputs.photoCollectionViewDataObservable
            .map({ [UploadImageSection(header: "사진업로드 컬렉션뷰 헤더", items: $0)]
            })
            .bind(to: self.photoCollectionView.rx.items(dataSource: dataSource))
            .disposed(by: disposeBag)

[참조]:
RxSwift 6. merge, combineLatest, withLatestFrom, zip, concat,
ReactiveX - From operator
RxSwift Observable, just, of, from

profile
james, the enthusiastic developer

0개의 댓글