기존 프로토콜에서 Completion Handler로 넘겨주던 값을 Observable로 넘겨주도록 함.
protocol MovieService {
func fetchMovies(page: Int) -> Observable<MovieResponse>
}
performRequest의 값을 디코딩 후 Observable에 data를 담아줌.
func fetchMovies(page: Int) -> Observable<MovieResponse> {
var urlComponents = URLComponents(string: Constants.URL.baseURL)
urlComponents?.path = Constants.URL.popularMoviesPath
urlComponents?.queryItems = [
URLQueryItem(name: Constants.URL.apiKeyName, value: APIKey.apiKey),
URLQueryItem(name: "page", value: "\(page)")
]
guard let url = urlComponents?.url else {
return Observable.error(NetworkError.requestFailed)
}
return Observable.create { [weak self] observer in
self?.performRequest(url: url) { result in
switch result {
case .success(let data):
do {
let decoder = JSONDecoder()
let movieResponse = try decoder.decode(MovieResponse.self, from: data)
observer.onNext(movieResponse)
observer.onCompleted()
} catch {
observer.onError(NetworkError.decodeFailed)
}
case .failure(let error):
observer.onError(error)
}
}
return Disposables.create()
}
}
observable에 담겨진 값을 ViewModel에서 BehaviorRelay에 담아줌.
Main thread 에서 observe함. 이를 통해 UI 업데이트를 메인스레드에서 실행하도록 보장함.
구독 시점으로부터 가장 최근 값을 처리할 수 있음.
class ViewModel {
// (중략)
private let disposeBag = DisposeBag()
let movieRelay = BehaviorRelay<[Movie]>(value: [])
var movies: Observable<[Movie]> {
return movieRelay.asObservable()
}
func fetchMovies(page: Int) {
movieService.fetchMovies(page: page)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] response in
self?.appendMovies(response.results)
}, onError: { error in
print("Failed to fetch movies: \(error)")
}).disposed(by: disposeBag)
}
private func appendMovies(_ newMovies: [Movie]) {
var currentMovies = movieRelay.value
currentMovies.append(contentsOf: newMovies)
movieRelay.accept(currentMovies)
}
}
ViewController의 viewdidload에서 ViewModel의 movie를 구독함.
mainView.activityIndicator.startAnimating()
viewModel.movies
.subscribe(onNext: { [weak self] movies in
self?.mainView.collectionView.reloadData()
self?.mainView.activityIndicator.stopAnimating()
})
.disposed(by: disposeBag)
viewModel.fetchMovies(page: page)
구독을 통해 movieRelay가 비동기적으로 업데이트 됨을 확인할 수 있음.
UI 바인딩을 사용해 상태 변경을 UI에 반영되도록함.
func bindUI(){
viewModel.movies
.bind(to: mainView.collectionView.rx.items(cellIdentifier: Constants.CellInfo.mainCellIdentifier, cellType: MainViewCell.self)) {
row, movie, cell in
cell.label.text = movie.title
if let posterPath = movie.posterPath {
let imageURL = Constants.URL.posterPath + posterPath
cell.setImage(urlString: imageURL)
}
}.disposed(by: disposeBag)
}
movieRelay의 변경사항이 자동으로 Collectionview에 자동으로 바인딩되어 movieRelay가 업데이트 될때마다 자동으로 CollectionView가 업데이트 됨.