MVVM Inout패턴
ViewController -> ViewModel로 데이터나 이벤트를 Input으로 전달, ViewModel -> ViewController로 데이터나 이벤트를 Output으로 전달하는 구조. Input, Output으로 입출력이 명확하게 나뉘어지기에 유지보수나 가독성을 향상시킨 디자인 패턴
예시 - 아이튠즈 검색
구조
Input: SearchBarText, SearchBarTapSearchButton 이벤트
Output: isLoading, searchError, Search Result
시행착오
tapSearchButton 이벤트 스트림으로 에러처리 한 경우
func transform(input: Input) -> Output {
let searchResult: PublishRelay<[AppInfo]> = .init()
let searchError: PublishRelay<Error> = .init()
input.tapSearchButton.withLatestFrom(input.searchText) { _ , query in
return query
}.subscribe(on: MainScheduler.instance)
.flatMap { self.searchKeyword(keyword: $0) }
.subscribe(with: self) { owner, result in
owner.isLoading.accept(false)
owner.searchResult.append(contentsOf: result)
searchResult.accept(owner.searchResult)
} onError: { owner, error in
owner.isLoading.accept(false)
searchError.accept(error)
}.disposed(by: disposeBag)
return Output(isLoading: isLoading.asDriver(), searchResult: searchResult.asDriver(onErrorJustReturn: []), searchError: searchError.asDriver(onErrorJustReturn: APIError.unknownResponse))
}
private func searchKeyword(keyword: String) -> Observable<[AppInfo]> {
isLoading.accept(true)
return searchService.fetchBoxOfficeData(searchKeyword: keyword)
}
- 이벤트를 받아 flatMap으로 검색 수행, 이후 subscribe로 오류 및 에러처리
- 하지만 오류가 발생했을 경우 tapSearchButton이 dispose되면서 이후에 이벤트를 받을 수 없음
- catch, catchReturns은 해당 스트림을 그대로 유지시켜 주지만, 기존에 subscribe를 하던 Observable을 dispose하고, 새로운 Observable은 리턴 해줌 -> tapSearchButton이 dispose되면서 동일한 문제 발생
- drive역시 에러를 안받는다고 하기에 에러로 인한 dispose가 없다는 뜻으로 이해했으나.. 베이스는 Observable이기에 driver도 error받으면 dispose됨
따라서 tapSearchButton 스트림에서는 검색만 수행, viewModel 내부에 변수를 추가하여 output에 해당 변수를 전달하는 방식으로 구현.
import Foundation
import RxCocoa
import RxSwift
protocol BaseViewModel {
associatedtype Input
associatedtype Output
var disposeBag: DisposeBag { get set }
func transform(input: Input) -> Output
}
class SearchViewModel: BaseViewModel {
private var searchResult: [AppInfo] = []
var searchResultRelay = PublishRelay<[AppInfo]>()
var searchError = PublishRelay<Error>()
var disposeBag = DisposeBag()
private let isLoading: BehaviorRelay<Bool> = .init(value: false)
private let currentError: PublishRelay<Error> = .init()
let searchService = SearchService()
struct Input {
let searchText: ControlProperty<String>
let tapSearchButton: ControlEvent<Void>
}
struct Output {
let isLoading: Driver<Bool>
let searchResult: Driver<[AppInfo]>
let searchError: Driver<Error>
}
func transform(input: Input) -> Output {
input.tapSearchButton.withLatestFrom(input.searchText) { _ , query in
return query
}.bind(with: self) { owner, query in
owner.searchKeyword(keyword: query)
}.disposed(by: disposeBag)
return Output(isLoading: isLoading.asDriver(), searchResult: searchResultRelay.asDriver(onErrorJustReturn: []), searchError: searchError.asDriver(onErrorJustReturn: APIError.unknownResponse))
}
private func searchKeyword(keyword: String){
isLoading.accept(true)
searchService.fetchBoxOfficeData(searchKeyword: keyword).subscribe(with: self) { owner, result in
owner.isLoading.accept(false)
owner.searchResultRelay.accept(result)
} onError: { owner, error in
owner.isLoading.accept(false)
owner.searchError.accept(error)
}.disposed(by: disposeBag)
}
}
구현은 하기는 했으나 뭔가 Rx스럽지않는다는 느낌이 든다. 중간에 stream이 끊기니까 굳이 Rx로 작성할 이유가 없어보인다.. 조만간 멘토님께 물어봐서 좀 고쳐야 할 듯 하다.
'iOS > 앱 개발' 카테고리의 다른 글
URLSession으로 multipart/form 방식으로 데이터 전송 (0) | 2023.12.17 |
---|---|
권한 요청, 설정 이동을 추상화(protocol)를 이용하여 재사용 가능하게 사용하기 (1) | 2023.11.21 |
렙케어 출시 프로젝트 회고 (0) | 2023.11.07 |
Swift의 namespace (0) | 2023.07.30 |
원시값을 감싸서 유지보수하기 쉬운 코드를 만들어보자 (0) | 2023.06.26 |