티스토리 뷰

iOS

[iOS] API 호출하기 (Advanced)

Kim_Baechu 2021. 2. 1. 17:10

서로 다른 Model, Enpoint를 갖는 두 가지 get을 한 개의 function을 이용해서 사용해보겠습니다.

 

아래 사이트에서 제공하는 OpenAPI를 사용하였습니다.

dog.ceo

 

Dog CEO. Good Dog Business.

Leaders in the dog and canine business world.

dog.ceo

 

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는 아직 어렵다ㅠㅠ

댓글
공지사항