# Network Requests and REST APIs in iOS

매번 똑같은 코드를 복사하고 붙여놓았는데 코딩 면접때 API 관련 질문을 무조건 할 것 같아서 글을 읽어보았다.

글을 읽으면서 유용하게 생각했던 정보들을 정리해보자면

# Three fundamental types

# class URLSession

Represents on HTTP session. Used to make individual HTTP requests that share configuration and cocking.

# struct URLRequest

Represents on HTTP request. Includes parameters like the URL, the HTTP method, and the HTTP headers.

평소에 get을 사용하기 떄문에 따로 설정해줄 필요가 없다. 다만 RxSwift 경우 URLRequest를 넣어주어여 URLSessionTask에 넣어주어야 사용 가능하다.

# class URLSessionTask

performs the data transfer for a network request. You don't use this class directly, but one of its subclasses like URLSessionDataTask.

# model을 효과적으로 이용하는 방법

struct WeatherData: Decodable {
    let name: String
    let main: Main
    let weather: [Weather]
    
    var model: WeatherModel {
        // viewcontrol과 연결할 부분, api에서 fetch를 한 다음, 여기서 바로 데이터를 가공할 수 있다. 
        return WeatherModel(countryName: name,
                            temp: main.temp.toInt(),
                            conditionId: weather.first?.id ?? 0,
                            conditionDescription: weather.first?.description ?? "")
    }
}

struct Main: Decodable {
    let temp: Double
}

struct Weather: Decodable {
    let id: Int
    let main: String
    let description: String
}

struct WeatherModel {
    // 이미 fetch 된 데이터를 이용하기 떄문에 Decodable이 필요없다. 
    // viewcontrol과 연결할 부분, api에서 fetch를 한 다음, 여기서 바로 데이터를 가공할 수 있다. 
    let countryName: String
    let temp: Int
    let conditionId: Int
    let conditionDescription: String
    
    var conditionImage: String {
        switch conditionId {
        case 200...299:
            return "imThunderstorm"
        case 300...399:
            return "imDrizzle"
        case 500...599:
            return "imRain"
        case 600...699:
            return "imSnow"
        case 700...799:
            return "imAtmosphere"
        case 800:
            return "imClear"
        default:
            return "imClouds"
        }
    }
}

# CodingKey

CodingKey를 이용하면 model 파일 struct 안에서 JSON 파일과 동일한 이름을 가질 필요가 없다. struct 안에서는 좀 더 직관적인 이름으로 하고 codingKey를 통해 매칭을 해주면 된다.

struct User {
	let name: String?
	let reputation: Int?
	let profileImageURL: URL?
	var profileImage: UIImage?
}
 
 // CodingKey 부분에만 decondable를 넣어주기 
extension User: Decodable {
	enum CodingKeys: String, CodingKey {
		case reputation
		case name = "display_name"
		case profileImageURL = "profile_image"
	}
}

# 까먹을까봐 적는 코드

VIPER 구조에서는 대충 이렇게 적용가능. 진짜 대충 적음

enum ServiceError: Error {
    case unknown
    case urlTransformFailed
    case requestFailed(response: HTTPURLResponse, data: Data?)
}

func load(with completion: @escaping (Result<Question, ServiceError>) -> Void) {
    guard let url = URL(string: "") else { return }
    let task = URLSession.shared.dataTask(with: url) { (data, _, error) in
        guard let data = data, error == nil else {
            completion(.failure(ServiceError.unknown))
            return
        }
        
        do {
            let decoded = try JSONDecoder().decode(Question.self, from: data)
            completion(.success(decoded))
        } catch {
            completion(.failure(ServiceError.unknown))
        }
    }
    task.resume()
}

func interactorDidFetchUsers(with result: Result<[User], Error>) {
    switch result {
    case .success(let users):
        view?.update(with: users)
    case .failure:
        view?.update(with: "Something went wrong")
    }
}

# rxswift에서는 대충 이렇게

MVVM 구조에서는 이렇게

//ServiceResult.swift
enum ServiceError: Error {
    case unknown
    case urlTransformFailed
    case requestFailed(response: HTTPURLResponse, data: Data?)
}

//Reactive+Base.swift
// session.rx.data 같은 경우 observable를 반환하기 때문에 single 타입의 extension으로 새로운 함수를 만들어준다. 
extension Reactive where Base: URLSession {
    func dataTask(request: URLRequest) -> Single<Data> {
        return Single.create(subscribe: { observer -> Disposable in
            
            let task = self.base.dataTask(with: request) { (data, response, error) in
                
                guard let response = response as? HTTPURLResponse, let data = data else {
                    observer(.failure(error ?? ServiceError.unknown))
                    return
                }
                
                guard 200..<400 ~= response.statusCode else {
                    observer(.failure(error ?? ServiceError.requestFailed(response: response, data: data)))
                    return
                }
                observer(.success(data))
                
            }
            task.resume()
            return Disposables.create(with: task.cancel)
        })
    }

//  Data+Decode.swift
extension Data {
    func decode<T>(_ type: T.Type, decoder: JSONDecoder? = nil) throws -> T where T: Decodable {
        let decoder = decoder ?? JSONDecoder()
        return try decoder.decode(type, from: self)
    }
}
//  APIService.swift
protocol APIServiceType {
    func fetchData() -> Single<[ArticleContent]>
}

struct APIService: APIServiceType {
    
    //MARK: - Properties
    
    private let session: URLSession
    
    //MARK: - Initialize
    
    init(session: URLSession = URLSession.shared) {
        self.session = session
    }
    
    // MARK: - DataTask
    
    func fetchData() -> Single<[ArticleContent]> {
        let baseUrl = ""
        
        guard let url = URL(string: baseUrl) else { return .error(ServiceError.urlTransformFailed) }
        
        return session.rx.dataTask(request: URLRequest(url: url))
            .map { data throws in
                let articles = try data.decode(Article.self)
                return articles.articles
                
            }
    }
}
//  MainViewModel.swift
let articles: Driver<[ArticleData]>

articles = Observable<Void>
    .merge([viewWillAppear, didTapLeftBarButton, didPulltoRefresh])
    .observe(on: ConcurrentDispatchQueueScheduler(qos: .default))
    .do(onNext: { _ in onNetworking.onNext(true)})
    .flatMap {
        return apiService.fetchData()
            .retry(2)
            .do(onDispose:  { onNetworking.onNext(false) })
            .catch({ error -> Single<[ArticleContent]> in
                onError.onNext(error)
                return .never()
            })
    }
    .map { [ArticleData(model: "", items: $0)] }
    .asDriver(onErrorJustReturn: [])

MVC에서는 대충 이렇게

//  WeatherResult.swift

struct WeatherResult: Codable {
    let main: Weather
}

extension WeatherResult {
    //이런 방식으로 오류가 일어날 경우를 미리 지정해줄 수 있다.
    static var empty: WeatherResult {
        return WeatherResult(main: Weather(temp: 0.0, humidity: 0.0))
    }
}

struct Weather: Codable {
    let temp: Double
    let humidity: Double
}

//  URLRequest+Extensions.swift
struct Resource<T> {
    let url: URL
}

extension URLRequest {
    static func load<T: Decodable>(resource: Resource<T>) -> Observable<T> {
        
        return Observable.from([resource.url])
            .flatMap { url -> Observable<Data> in
                let request = URLRequest(url: url)
                return URLSession.shared.rx.data(request: request)
            }.map { data -> T in // data throws in // 이라고 적을 수 있다 
                return try JSONDecoder().decode(T.self, from: data)
            }
    }
}

//  ViewController.swift
let resource = Resource<WeatherResult>(url: url)

let search = URLRequest.load(resource: resource)
    .observe(on: MainScheduler.instance)
    .asDriver(onErrorJustReturn: WeatherResult.empty)

search.map { "\($0.main.temp) 🌡" }
    .drive(self.temperatureLabel.rx.text)
    .disposed(by: disposeBag)

# 이렇게는 하지마

괜찮아 보여도 이렇게는 하지마

class NetworkManager {
	func load<T>(url: URL, withCompletion completion: @escaping (T?) -> Void) {
		let task = URLSession.shared.dataTask(with: url) { (data, _, _,) -> Void in
			guard let data = data else {
				DispatchQueue.main.async { completion(nil) }
				return
			}
			switch T.self {
			case is UIImage.Type:
				DispatchQueue.main.async { completion(UIImage(data: data) as? T) }
			case is Question.Type:
				let wrapper = try? JSONDecoder().decode(Wrapper<Question>.self, from: data)
				DispatchQueue.main.async { completion(wrapper?.items[0] as? T) }
			case is [Question].Type:
				let wrapper = try? JSONDecoder().decode(Wrapper<Question>.self, from: data)
				DispatchQueue.main.async { completion(wrapper?.items as? T) }
			default: break
			}
		}
		task.resume()
	}
}

이렇게 작게 쪼개기

class NetworkManager {
	func load(url: URL, withCompletion completion: @escaping (Data?) -> Void) {
		let task = URLSession.shared.dataTask(with: url) { (data, _, _,) -> Void in
			DispatchQueue.main.async { completion(data) }
		}
		task.resume()
	}
	
	func loadImage(with url: URL, completion: @escaping (UIImage?) -> Void) {
		load(url: url) { data in
			if let data = data {
				completion(UIImage(data: data))
			} else {
				completion(nil)
			}
		}
	}
	
	func loadTopQuestions(completion: @escaping ([Question]?) -> Void) {
		let url = URL(string: "https://api.stackexchange.com/2.2/questions?order=desc&sort=votes&site=stackoverflow")!
		load(url: url) { data in
			guard let data = data else {
				completion(nil)
				return
			}
			let wrapper = try? JSONDecoder().decode(Wrapper<Question>.self, from: data)
			completion(wrapper?.items)
		}
	}
}

# 이렇게 코딩할 수 있으면 좋겠다

//
//  NetworkRequest.swift
//  TopQuestion
//
//  Created by Matteo Manferdini on 12/09/2019.
//  Copyright © 2019 Matteo Manferdini. All rights reserved.
//

import Foundation
import UIKit

protocol NetworkRequest: AnyObject {
	associatedtype ModelType
	func decode(_ data: Data) -> ModelType?
	func load(withCompletion completion: @escaping (ModelType?) -> Void)
}

extension NetworkRequest {
	fileprivate func load(_ url: URL, withCompletion completion: @escaping (ModelType?) -> Void) {
		let session = URLSession(configuration: .default, delegate: nil, delegateQueue: .main)
		let task = session.dataTask(with: url, completionHandler: { [weak self] (data: Data?, response: URLResponse?, error: Error?) -> Void in
			guard let data = data else {
				completion(nil)
				return
			}
			completion(self?.decode(data))
		})
		task.resume()
	}
}

class ImageRequest {
	let url: URL
	
	init(url: URL) {
		self.url = url
	}
}

extension ImageRequest: NetworkRequest {
	func decode(_ data: Data) -> UIImage? {
		return UIImage(data: data)
	}
	
	func load(withCompletion completion: @escaping (UIImage?) -> Void) {
		load(url, withCompletion: completion)
	}
}

class APIRequest<Resource: APIResource> {
	let resource: Resource
	
	init(resource: Resource) {
		self.resource = resource
	}
}

extension APIRequest: NetworkRequest {
	func decode(_ data: Data) -> [Resource.ModelType]? {
		let wrapper = try? JSONDecoder().decode(Wrapper<Resource.ModelType>.self, from: data)
		return wrapper?.items
	}
	
	func load(withCompletion completion: @escaping ([Resource.ModelType]?) -> Void) {
		load(resource.url, withCompletion: completion)
	}
}

protocol APIResource {
	associatedtype ModelType: Decodable
	var methodPath: String { get }
}

extension APIResource {
	var url: URL {
		var components = URLComponents(string: "https://api.stackexchange.com/2.2")!
		components.path = methodPath
		components.queryItems = [
			URLQueryItem(name: "site", value: "stackoverflow"),
			URLQueryItem(name: "order", value: "desc"),
			URLQueryItem(name: "sort", value: "votes"),
			URLQueryItem(name: "tagged", value: "ios")
		]
		return components.url!
	}
}

struct QuestionsResource: APIResource {
	typealias ModelType = Question
	let methodPath = "/questions"
}

# References

Network Requests and REST APIs in iOS with Swift (Protocol-Oriented Approach) (opens new window)