봄 섹션의 카드 셀을 눌렀을 때만 미리듣기가 재생되고, 다른 섹션을 눌렀을 때는 미리듣기가 재생되지 않도록 구현하였다.
그런데 다른 섹션을 눌렀을 때 Alert을 띄운 후, 다시 다른 셀을 누르면 어떠한 동작도 하지 않는 현상을 발견했다.
기존 코드
final class HomeViewModel: ViewModel {
func transform(_ input: Input) -> Output {
let target = input.playMusic
.flatMap { item in
self.fetchPreviewTarget(item: item)
}
return Output( ..., musicPreviewTarget: target)
}
}
extension HomeViewModel {
private func fetchPreviewTarget(item: MusicCollectionView.Item?) -> Observable(isNew: Bool, music: Music)> {
Observable.create { [weak self] observer in
guard let self else { return Disposables.create() }
switch item {
case .spring(let music): // 봄 섹션일 경우 onNext 방출
observer.on(.next((isNew, preview)))
observer.on(.completed)
default:
observer.onError(TargetError.invalidTarget)
}
return Disposables.create()
}
}
}
기존 코드에서는 fetchPreviewTarget 함수를 호출하여 봄 섹션이 아닐 경우 onError 이벤트를 방출하고 있다.
class HomeViewController: UIViewController {
private func bind() {
output.musicPreviewTarget
.subscribe(
onNext: { [weak self] target in
self?.playPreview(of: target.music, isNew: target.isNew)
}, onError: { [weak self] error in
self?.showAlert(title: "Error", message: "대상 파일을 찾지 못했습니다.")
})
.disposed(by: disposeBag)
}
}
이후 VC에서 onError일 경우 alert를 띄워 처리하고 있다.
여기서! Observable 객체가 onError를 방출해버리면 해당 스트림이 종료된다고 한다.
onError가 방출되면서 disposed 되기 때문에 반응형 동작을 다시 수행시키려면 다시 구독을 해주어야 한다.
viewModel에서 봄 섹션이 아닐 경우 onError를 방출하기 때문에 alert을 띄운 후 아무런 동작이 되지 않았던 것이다.
기존에는 ViewModel에서 target을 Observable<(isNew: Bool, music: Music)> 타입으로 방출하고 있었다.
이 방출 타입을 Result 타입으로 감싸서 방출해보자.
final class HomeViewModel: ViewModel {
let target: Observable<Result<(isNew: Bool, music: Music), Error>> = input.playMusic
.withUnretained(self) // guard let self 대신 사용 가능한 편의성 메서드
.flatMap { `self`, item in
.map {
.success($0)
}
.catch {
.just(.failure($0))
}
}
return Output(..., musicPreviewTarget: target)
}
기존의 fetchPreviewTaret함수는 변함이 없다.
단, 위 코드는 flatMap 내부에서 fetchPreviewTarget 함수의 결과를 처리하여 Result타입으로 감싸준다.
fetchPreviewTarget의 결과가 onNext라면, map 연산으로 들어가 Result의 success 타입으로 감싸진다.
fetchPreviewTarget의 결과가 onError라면, catch 연산으로 들어가 Result의 failure 타입으로 감싸진다.
error는 한번만 방출되므로 just로 한번 더 감싸준다.
class HomeViewController: UIViewController {
private func bind() {
output.musicPreviewTarget
.subscribe(
onNext: { [weak self] target in
switch target {
case .success((let isNew, let music)):
self?.playPreview(of: music, isNew: isNew)
case .failure(let error):
self?.showAlert(title: "Error", message: "대상 파일을 찾지 못했습니다.")
}
})
.disposed(by: disposeBag)
}
}
이제 musicPreviewTarget은 Observable<Result<(isNew: Bool, music: Music)>> 타입이므로 VC에서는 해당 타입을 구독할 때 onNext에서 Result타입의 처리를 해준다.
musicPreviewTarget은 onError를 반환하지 않으므로 onNext의 처리만 해주면 된다.

잘 작동하는 것을 확인하였다.
💡 Unretained(self)
ViewModel의 코드를 수정하면서 .withUnretained(self)를 사용하였다.final class HomeViewModel: ViewModel { let target: Observable<Result<(isNew: Bool, music: Music), Error>> = input.playMusic .withUnretained(self) // guard let self 대신 사용 가능한 편의성 메서드 .flatMap { `self`, item in ... } ... }.withUnretained는 RxSwift에서 제공하는 메서드로, 괄호 안의 객체(여기서는 self라 칭함)를 참조하되, 강한 참조로는 하지 않고 가져올 수 있다.
만약 해당 시점에 self가 존재하지 않는다면 다음 연산으로 넘어가지 않고 .empty()를 반환한다.
func withUnretained<Object: AnyObject, Out>( _ obj: Object, resultSelector: @escaping (Object, Element) -> Out ) -> Observable<Out> { map { [weak obj] element -> Out in guard let obj else { throw UnretainedError.failedRetaining } return resultSelector(obj, element) } .catch { error -> Observable<Out> in guard let unretainedError = error as? UnretainedError, unretainedError == .failedRetaining else { return .error(error) } return .empty() } }코드 내부를 살펴보면 괄호 안의 객체는 [weak obj]로 약한 참조를 하고 있다.
내부에서 guard문으로 예외처리를 해주고 있는데, 기존에 우리가guard let self else { return }하던 것을 대신 해주고 있다고 생각하면 될 것 같다.덕분에 해당 메소드를 사용하면 guard문 없이 self를 사용 가능하다.
단! 클로저에서 변수 선언시 백틱으로 감싸주어야한다.(``)