RxSwift

주방·2023년 5월 17일
0

MovieRank

목록 보기
8/8
post-thumbnail

배경

  • 기능 구현이 완료된 후 발생하는 오류를 수정하고 있음.
  • 먼저는 image caching 처리를 통해 비동기 처리를 하고, 코드의 복잡도를 낮췄음.(kingfisher 사용)
  • 그렇다면 현재 data에 대해서도 비동기 처리를 할 수 없을까?
  • 현재 View Controller의 복잡도를 낮출 수 없을까? CollectionView의 delegate, datasource를 통해 데이터를 전달하는데 이를 비동기적으로 처리할 수 없을까?
  • UIKit에서는 RxSwift를 통해 비동기처리를 할 수 있음을 알게 됨.

RxSwift을 위한 생각의 흐름

  1. 데이터 전달 방식을 비동기적으로 처리하고자 함.
  2. 기존 코드의 경우 ViewModel에서 movie를 설정하였음.
  3. ViewController에서 ViewModel의 인스턴스를 생성하여 해당 메소드 및 프로퍼티를 사용하였음.
  4. 그렇다면 해당 모델(movie)를 RxSwift에서 제공하는 프로퍼티(observable, subject - 구독할 수 있는 대상)으로 담아 View Controller에서 해당 프로퍼티를 구독하여 비동기적으로 처리할 수 있지 않을까 생각했음.

RxSwift 코드적용

  1. 기존 프로토콜에서 Completion Handler로 넘겨주던 값을 Observable로 넘겨주도록 함.

  2. protocol MovieService {
        func fetchMovies(page: Int) -> Observable<MovieResponse>
    }
  3. performRequest의 값을 디코딩 후 Observable에 data를 담아줌.

  4. 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()
            }
        }
  5. observable에 담겨진 값을 ViewModel에서 BehaviorRelay에 담아줌.

  6. Main thread 에서 observe함. 이를 통해 UI 업데이트를 메인스레드에서 실행하도록 보장함.

  7. 구독 시점으로부터 가장 최근 값을 처리할 수 있음.

  8. 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)
        }
    }
  9. ViewController의 viewdidload에서 ViewModel의 movie를 구독함.

  10. 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)
  11. 구독을 통해 movieRelay가 비동기적으로 업데이트 됨을 확인할 수 있음.

  12. UI 바인딩을 사용해 상태 변경을 UI에 반영되도록함.

  13. 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)
        }
  14. movieRelay의 변경사항이 자동으로 Collectionview에 자동으로 바인딩되어 movieRelay가 업데이트 될때마다 자동으로 CollectionView가 업데이트 됨.


정리

  1. Observable, Subject를 활용하여 데이터를 업데이트 하였음.
  2. 데이터 변경될 때마다 바인딩하여 뷰가 자동으로 업데이트 할 수 있었음.
  3. View Controller 내부에서 RxSwif 관련 메소드를 extension으로 정리하고, collectionview의 datasource(didselect, cellforItemAt)를 사용하지 않을 수 있었음.
  4. 이를 통해 View Controller의 복잡성을 줄일 수 있었으며,

0개의 댓글