티스토리 뷰
서로 다른 Model, Enpoint를 갖는 두 가지 get을 한 개의 function을 이용해서 사용해보겠습니다.
아래 사이트에서 제공하는 OpenAPI를 사용하였습니다.
enum APIError: Error {
case response
}
enum URLType {
case list
case randomImage(String)
var baseURL: String {
return "https://dog.ceo/api/"
}
var makeURL: String {
switch self {
case .list:
return "\(baseURL)breeds/list/all"
case .randomImage(let breed):
return "\(baseURL)breed/\(breed)/images/random"
}
}
}
1. Error타입을 만들어줍니다.
2. URLType에서는 baseURL을 만들고 case에 따라 서로 다른 endpoint를 갖게 만들었습니다.
struct NetworkService {
static func loadData<T: Codable>(type: URLType, completion: @escaping (Result<T,APIError>) -> Void) {
AF.request(type.makeURL)
.responseDecodable(of: T.self) { response in
guard let target = response.value else {
return completion(.failure(.response))
}
completion(.success(target))
}
}
}
1. URLType으로 url을 받아오고 컴플리션에서 모델에 맞는 결과를 처리합니다.
2. T에는 Codable한 Model이 사용됩니다.
// MARK: - Dog
struct DogList: Codable {
let message: [String: [String]]
let status: String
}
// MARK: - ImageLink
struct ImageLink: Codable {
let message: String
let status: String
}
1. 각각의 모델은 Codable을 사용합니다.
리스트 불러오기
func setUpData() {
NetworkService.loadData(type: .list) { [weak self] (result: Result<DogList,APIError>) in
switch result {
case .success(let model):
self?.dogList = model.message.map { $0.key }.sorted(by: <)
self?.listTableView.reloadData()
case .failure(let error):
print(error)
}
}
}
1. DogList로 Json을 Decode합니다.
2. 딕셔너리 타입의 결과 중 key값만 정렬해서 배열로 만들어줍니다.
이름과 사진 불러오기
func setUpData() {
NetworkService.loadData(type: .randomImage(breed)) { [weak self] (result: Result<ImageLink,APIError>) in
switch result {
case .success(let model):
self?.imageLink = model.message
self?.imageTableView.reloadData()
case .failure(let error):
print(error)
}
}
}
1. ImageLink로 Json을 Decode합니다.
2. model의 message에 사진 링크가 있습니다.
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ImageCell", for: indexPath) as! ImageCell
cell.dogLabel.text = breed
cell.dogImageView.kf.setImage(with: URL(string: imageLink))
return cell
}
1. Kingfisher로 이미지를 불러왔습니다.
MVVM+Rx
아직 Rx가 익숙하지 않아서 잘못된 부분이 있을 수 있습니다.
혹시 제가 틀린 부분이 있으면 댓글로 알려주시면 정말 감사하겠습니다.
Single은 success, error 두 개의 이벤트를 처리합니다.
single에 관한 정보는 민소네님 블로그에서 자세히 공부하시면 좋을 것 같습니다.
아래코드는 iOS개발자 허광호님의 도움을 받아 작성했습니다.
struct RxNetworkService {
static func loadData<T: Codable>(type: URLType) -> Single<T> {
guard let url = URL(string: type.makeURL) else { return Observable.error(NSError(domain: "url generation error", code: -1, userInfo: nil)).asSingle() }
var request = URLRequest(url: url)
request.httpMethod = "GET"
return Single<T>.create { (single) -> Disposable in
let task = URLSession.shared.dataTask(with: request) { data, responds, error in
if let error = error {
single(.error(error))
return
}
if let model: T = try? JSONDecoder().decode(T.self, from: data ?? Data()) {
single(.success(model))
} else {
print("decoding error")
}
}
task.resume()
return Disposables.create {
task.cancel()
}
}
}
}
ListViewModel
class RxListViewModel {
var dogList = [String]()
let dogListRelay = BehaviorRelay<[String]>(value: [])
let disposeBag = DisposeBag()
func setupData() {
let result: Single<DogList> = RxNetworkService.loadData(type: .list)
result.subscribe { [weak self] event in
guard let self = self else { return }
switch event {
case .success(let model):
self.dogList = model.message.map { $0.key }.sorted(by: <)
self.dogListRelay.accept(self.dogList)
case .error(let error):
print(error.localizedDescription)
}
}.disposed(by: disposeBag)
}
}
1. 전달받은 데이터를 DogList로 Decode하고 배열로 만들어서 dogListRelay로 만들었습니다.
ListViewController
func bindingViewModel() {
viewModel.dogListRelay
.bind(to: listTableView.rx.items(cellIdentifier: "RxCell", cellType: RxCell.self)) { _, dog, cell in
cell.dogLabel.text = dog
}.disposed(by: disposeBag)
listTableView.rx.modelSelected(String.self)
.subscribe(onNext: {string in
let storyBoard = UIStoryboard(name: "Main", bundle: nil)
let vc = storyBoard.instantiateViewController(identifier: "RxImageViewController") as! RxImageViewController
vc.viewModel.dog = string
self.navigationController?.pushViewController(vc, animated: true)
}).disposed(by: disposeBag)
}
1. tableView에 dogListRelay를 바인딩했습니다.
2. 셀을 선택하면 해당 종의 랜덤 이미지를 보여주는 화면으로 넘어갑니다.
RxImageViewModel
class RxImageViewModel {
var breed = ""
var dogRelay = PublishRelay<[String: ImageLink]>()
let disposeBag = DisposeBag()
func setupData() {
let result: Single<ImageLink> = RxNetworkService.loadData(type: .randomImage(breed))
result.subscribe { [weak self] event in
guard let self = self else { return }
switch event {
case .success(let model):
self.dogRelay.accept([self.breed: model])
case .error(let error):
print(error.localizedDescription)
}
}.disposed(by: disposeBag)
}
}
1. Json에 breed관련 값이 없어서 PublishRelay<[ImageLink]>()가 아니라 PublishRelay<[String: ImageLink]>()로 만들어서 breed를 추가해줬습니다.
RxImageViewController
func bindingViewModel() {
viewModel.dogRelay
.observeOn(MainScheduler.instance)
.bind(to: imageTableView.rx.items(cellIdentifier: "RxImageCell", cellType: RxImageCell.self)) { _, model ,cell in
cell.dogLabel.text = model.key
cell.dogImageView.kf.setImage(with: URL(string: model.value.message))
}.disposed(by: disposeBag)
}
사용 후 느낀점
1. 중복코드를 제거하고 재사용성을 높일 수 있었습니다.
2. 코드의 가독성이 좋아졌습니다.
3. Rx는 아직 어렵다ㅠㅠ
'iOS' 카테고리의 다른 글
[iOS] Alamofire 순서대로 API 실행하기 (0) | 2021.02.23 |
---|---|
[iOS] HLS(HTTP Live Streaming) 영상 스트리밍 알아보기 (0) | 2021.02.05 |
[iOS] UIView layer로 뷰 그리기, 그림자 (0) | 2021.01.29 |
[iOS] frame과 bounds (1) | 2021.01.20 |
[iOS] Operation 알아보기 (0) | 2021.01.20 |