// before
var pokemonId: String {
guard let firstIndex = url.dropLast().lastIndex(of: "/") else { return "" }
return String(url[firstIndex..<url.endIndex].dropFirst().dropLast())
}
이건 내 기존 코드인데, 솔직히 처음에 이걸 썼을 때도 내가 생각해낼 수 있는 유일한 방법이어서 썼지 계속 drop
을 하는 게 썩 마음에 들진 않았다. 그래서 다른 동기들은 어떻게 구현했는지 구경하러 다니다가 마음에 드는 코드를 발견해서 따라쓴다고 말하고 따라썼다😁 히히
// after
var pokemonId: String {
guard let id = url.split(separator: "/").last else { return "" }
return String(id)
}
split
을 왜 생각해내지 못했을까? 하여튼 저걸 떠올리지 못해서 뒤에서 두번째 /
와 마지막 /
사이의 값을 빼내겠다고 저 난리를 피웠던 것이다. 그래도 재밌었다.
// before
typeLabel.text = "타입 : \(pokemon.types[0].type.translatedType)"
if pokemon.types.count > 1 {
typeLabel.text?.append(", \(pokemon.types[1].type.translatedType)")
}
사실 포켓몬 타입이 최대 2개라는 것을 확인했기 때문에 그냥 하나 더 있으면 그 뒤에 ", 타입"
을 붙이자는 의도에서 작성한 코드긴 하다. 하지만 이건 만약 값이 업데이트 되어 3개 이상이 되었을 때는 수정을 해야하는 코드인 것이다. 그래서 수정이 필요없는 더 좋은 코드로 수정했다. 이 코드 역시 위의 코드를 쓴 같은 동기가 쓴 것이다. (매우 고마운 마음을 전하는 바다.)
// after
typeLabel.text = "타입 : \(pokemon.types.map { $0.type.translatedType }.joined(separator: ", "))"
깔끔 편안 동기사랑 나라사랑
과제 가이드에서 포켓몬 이름 번역 코드를 제공해주었는데, 당시 포켓몬 API의 응답과 현재 응담 데이터의 형태에 차이가 있나보다. 몇몇 포켓몬 이름들이 달라져 있었다.
"nidoran♀": "니드런♀"
"mr. mime": "마임맨"
이런 식으로 한 다여섯개 발견해서 수정했다. 근데 다른 동기가 자기는 30개도 더 넘게 고쳤다는 것이다😮 그래서 거래를 통해 번역 코드를 받았다. (금전 거래 아님) 다른 사람들과 교류하며 코드 이야기를 하는 것이 얼마나 좋은 건지 새삼 느낀 이번 과제였다.
필수 구현 기능을 다 마친 뒤 도전해볼 만한 구현 사항에는 다음이 있었다.
Relay를 활용
해보세요. (RxSwift
)- 이미지 라이브러리
Kingfisher를 활용
해보세요.
Kingfisher
는 이미 처음부터 썼기 때문에, 남은 건 Subject
와 Relay
의 차이를 알아보고 적용하는 것이었다.
// 기존 subject를 사용한 코드
class MainViewModel {
let pokemonList = PublishSubject<[PokemonResult]>()
func fetchPokemonList() {
offset += 20
provider.request(.fetchURL(offset: offset)) { [weak self] result in
switch result {
case .success(let response):
do {
let data = try JSONDecoder().decode(PokemonURL.self, from: response.data)
self?.pokemonList.onNext(data.results)
} catch {
self?.pokemonList.onError(NetworkError.decodingFailed)
}
case .failure(let error):
self?.pokemonList.onError(error)
}
}
}
}
class DetailViewModel {
let pokemonDetail = PublishSubject<Pokemon>()
func fetchPokemonDetail(_ urlString: String) {
guard let url = URL(string: urlString) else {
pokemonDetail.onError(NetworkError.invalidURL)
return
}
networkManager.fetch(url: url)
.subscribe(
onSuccess: { [weak self] (response: Pokemon) in
self?.pokemonDetail.onNext(response)
},
onFailure: { [weak self] error in
self?.pokemonDetail.onError(error)
}
)
.disposed(by: disposeBag)
}
}
Subject
는 RxSwift
에서 Observable
과 Observer
역할을 모두 수행할 수 있는 타입이다. 즉, 다른 Observable
을 구독함과 동시에 값을 방출할 수 있다. 위에 코드를 보면, 네트워크 통신을 통해 불러온 API response
값에 대해 통신 성공 시에는 onNext
, 실패 시에는 onError
를 통해 결과 값을 받는 것을 확인할 수 있다.
// relay로 변경한 코드
class MainViewModel {
let pokemonList = PublishRelay<[PokemonResult]>()
func fetchPokemonList() {
offset += 20
provider.request(.fetchURL(offset: offset)) { [weak self] result in
switch result {
case .success(let response):
do {
let data = try JSONDecoder().decode(PokemonURL.self, from: response.data)
self?.pokemonList.accept(data.results)
} catch {
print(error.localizedDescription)
}
case .failure(let error):
print(error.localizedDescription)
}
}
}
}
class DetailViewModel {
let pokemonDetail = PublishRelay<Pokemon>()
func fetchPokemonDetail(_ urlString: String) {
guard let url = URL(string: urlString) else { return }
networkManager.fetch(url: url)
.subscribe(
onSuccess: { [weak self] (response: Pokemon) in
self?.pokemonDetail.accept(response)
},
onFailure: { error in
print(error.localizedDescription)
}
)
.disposed(by: disposeBag)
}
}
Relay
역시 Subject
와 동일하게 Observable
과 Observer
역할을 모두 수행하는 타입이다. 하지만 Relay
는 RxCocoa
의 객체이며, 주로 UI
이벤트 처리에 사용한다. 코드를 보면, Subject
일 때는 onNext
로 구독한 값을 받아왔던 부분을 Relay
는 accept
를 통해 받는 것을 알 수 있다. Relay
는 값을 구독하고 방출하지만 에러
나 완료
이벤트를 방출하지 않기 때문에, 멈추지 않고 값을 받는다는 점에서 UI
를 그리는 데 유용한 것이다. (데이터 변경에 따라 UI
는 계속 업데이트 되어야하기 때문이다.) 이에 따라, Relay
를 구독하는 Controller
의 bind
코드에도 onError
이벤트 처리 부분이 삭제되었다.
네트워크 통신 라이브러리인 Moya
에서도 Rx
와 함께 사용할 수 있는 프레임워크를 제공한다. import RxMoya
를 하면 네트워트 통신 결과를 구독할 수 있다. MainViewModel
의 코드를 리팩토링 하였다.
class MainViewModel {
private let provider = MoyaProvider<PokemonAPI>()
let pokemonList = PublishRelay<[PokemonResult]>()
func fetchPokemonList() {
offset += 20
provider.rx.request(.fetchURL(offset: offset)
.map(PokemonURL.self) // decoding 코드 대체
.subscribe(
onSuccess: { [weak self] response in
self?.pokemonList.accept(response.results)
},
onFailure: { error in
print(error.localizedDescription)
}
)
.disposed(by: disposeBag)
}
}
do-catch
문으로 감싸 JSONDecoder
로 디코딩 했던 코드를 .map
한줄로 대체하니 코드의 가독성이 매우 좋아졌다. 아쉬움이 남는 건 error
처리를 세심하게 하지 못하고 print
에서 그친 것이다.
RxCocoa
는 UIKit
의 컴포넌트들에 대한 이벤트 처리를 할 수 있는 강력한 도구라고 한다. 처리 가능한 범위가 매우 넓은 것 같은데, 모두 다 공부하지는 못했고 이번에는 UICollectionViewDelegate
를 Rx
로 대체해보았다.
before
extension MainViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let vc = DetailViewController(vm: .init(pokemons[indexPath.item].url))
navigationController?.pushViewController(vc, animated: true)
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if scrollView.contentSize.height - scrollView.contentOffset.y == scrollView.visibleSize.height {
vm.fetchPokemonList()
}
}
}
after
private func bind() {
containerView.pokemonCollectionView.rx.itemSelected
.subscribe(
onNext: { [weak self] indexPath in
guard let self = self else { return }
let vc = DetailViewController(vm: .init(pokemons[indexPath.item].url))
navigationController?.pushViewController(vc, animated: true)
}
)
.disposed(by: disposeBag)
containerView.pokemonCollectionView.rx.didEndDecelerating
.subscribe(
onNext: { [weak self] in
guard let scrollView = self?.containerView.pokemonCollectionView else { return }
if scrollView.contentSize.height - scrollView.contentOffset.y == scrollView.visibleSize.height {
self?.vm.fetchPokemonList()
}
}
)
.disposed(by: disposeBag)
}
사실 프로젝트 규모가 크지 않으면 delegate
를 사용하는 것이 퍼포먼스, 메모리 효율 측면에서 더 낫다고 한다. 하지만 이번 과제가 Rx
를 연습해보는 게 취지인 만큼(그리고 RxCocoa
에서 이런 게 가능하다는 걸 처음으로 알아서 신기하기도 했고) 한번 리팩토링 해보았다.
UICollectionView
의 dataSource
를 구성할 때도 Rx
의 활용이 가능한 것 또한 알게 되었지만, 이번에 DiffableDataSource
또한 새로 공부하고 사용해 본 만큼 이 부분은 리팩토링을 진행하지 않았다.
로컬라이징은 사실 PokeAPI의 Pokemon species를 사용하면 간?단?하게 해결이 가능합니다