-
Notifications
You must be signed in to change notification settings - Fork 0
Tutorial(1) REST API RestManager
서버와 통신할 수 있게 연결해주는 REST API 코드를 가볍게 구현해보겠습니다.
구현을 시작하기 앞서 요청의 구조가 어떻게 되는 지 살펴보겠습니다 🙂
-
The endpoint : URL
-
The method → Http Method :
Get
Post
Put
Patch
Delete
-
The headers
→ 일반 헤더 : 리퀘스트와 리스폰스 모두 적용되지만 본문에서 전송되는 데이터와는 관련 X
→ 리퀘스트 헤더 : 가져올 리소스 또는 리소스를 요청하는 클라이언트에 대한 자세한 정보가 포함 O
→ 리스폰스 헤더 : 위치나 응답을 제공하는 서버와 같은 응답에 대한 추가 정보를 보유 O
→ 엔티티 헤더 : 콘텐츠 길이 또는 MIME 유형과 같은 리소스 본문에 대한 정보가 포함 O
-
The data(or body) → 서버로 보내려는 정보가 포함 O
이렇듯 서버와 통신할 수 있는 REST API를 구현하려면 위의 과정들이 필요합니다.
위 내용을 바탕으로 튜토리얼을 시작해보겠습니다.
먼저 우리는 extension으로 class RestManager에서 사용할 필요한 작업들을 해보겠습니다.
extension RestManager {
}
extension RestManager를 만드셨나요?
첫 번째로, 위에서 봤던 요청의 구조 중 하나인 Http Method
를 String 형태의 열거형으로 구현해보겠습니다.
enum HttpMethod: String {
case get
case post
case put
case patch
case delete
}
그 다음 헤더에 필요한 RestEntitiy 구조체를 만들어 데이터들의 값을 들고 있게 하겠습니다.
struct RestEntity {
// #1
private var values: [String: String] = [:]
// #2
mutating func add(
value: String,
forKey key: String) {
values[key] = value
}
// #3
func value(forKey key: String) -> String? {
return values[key]
}
// #4
func allValues() -> [String: String] {
return values
}
// #5
func totalItems() -> Int {
return values.count
}
}
#1
전송되는 데이터들은 키-값의 형태로 되어있어 우리는 values 라는 문자열 타입의 딕셔너리를 생성하고, 기능들을 여러가지 추가해보겠습니다.
#2
mutating이 생소하죠? 하지만 '(새로운 형태로) 변형되다'라는 뜻을 가지고 있습니다.
구조체 또는 열거형의 프로퍼티는 value type으로 이루어져있기 때문에 해당 인스턴스 메소드 내에서 값을 변경할 수 없습니다. 그러나 특정 메소드 내에서 구조체 또는 열거형의 프로퍼티를 수정해야 하는 경우, 해당 메소드의 동작을 변경(mutating)하도록 선택 할 수 있습니다.
또 딕셔너리의 문법을 알고 있으면 이해하기 쉬운데 새로운 딕셔너리를 추가할 때 values[key] = value
이런 식으로 써주면 된답니다😎
따라서 mutating을 써주면 새로운 딕셔너리를 추가할 때 문제 없겠죠?
#3
values안에 해당되는 [key] 값의 value를 보고싶을 때 사용하면 됩니다.
#4
values의 모든 딕셔너리를 나타내는 함수입니다.
#5
모든 딕셔너리의 갯수를 나타내는 함수입니다.
자, 그 다음에는 The headers에 필요한 Response headers를 구현해보겠습니다!
/// Response
struct Response {
// #1
var httpStatusCode: Int = 0
var headers = RestEntity()
var response: URLResponse?
// #2
init(fromURLResponse response: URLResponse?) {
guard let response = response else { return }
self.response = response
httpStatusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
if let headerFields = (response as? HTTPURLResponse)?.allHeaderFields {
for (key, value) in headerFields {
headers.add(
value: "\\(value)",
forKey: "\\(key)"
)
}
}
}
}
#1
요청 결과를 나타내는 httpStatusCode
또 구조체 RestEntity를 생성하는 headers
URLResponse(응답한 결과)를 들고 있는 response
를 선언해줍니다.
#2
URLResponse를 response와 fromURLResponse에 담고, 그 response를 언래핑해주는 작업을 해줍니다.
self.response = response -> 구조체 선언시 선언해둔 response는 위에 언래핑된 response를 씁니다.
'httpSatusCode가 nil이면 0, 아니면 http 상태코드를 던져줘'라고 구현합니다. (사실 httpSatusCode가 nil일 경우는 상태코드를 확인할 수 없음을 나타냅니다.)
HTTPURLResponse의 메소드 allHeaderFields는 아래 for문을 순회하며 RestEntity에 새로운 딕셔너리를 추가합니다.
이제 그 응답의 결과에 대해 구현해보겠습니다. 요청이 성공할 수도 있지만 실패할 수도 있기 때문에 에러 부분을 포함해서 코드를 짜야합니다.
// #1
///Error case
enum CustomError: Error {
case failedToCreateRequest
}
///Result
struct Results {
// #2
var data: Data?
var response: Response?
var error: Error?
//#3
init(
withData data: Data?,
response: Response?,
error: Error?
) {
self.data = data
self.response = response
self.error = error
}
init(withError error: Error) {
self.error = error
}
}
}
#1
프로토콜 Error를 준수하는 커스텀(사용자 지정) 에러로 나중에 에러가 나는 여러 경우들을 추가할 수 있게끔 열거형으로 선언해둡니다.
#2
실제 데이터를 받을 data
httpStatusCode, headers, URLResponse를 담은 Response 구조체를 resoponse
라는 변수에 선언
그리고 에러를 담을 error
→ 위 변수들은 옵셔널로 받아야 합니다.
#3
구조체와 클래스를 활용하고자 한다면 이니셜 라이저 과정이 꼭 필요합니다.
이니셜 라이저(initialiazers)란, 새로운 인스턴스를 만들어주는 과정이라고 할 수 있습니다.
이제 사용자 지정 오류에는
// #1
extension RestManager.CustomError: LocalizedError {
public var localizedDescription: String {
switch self {
case .failedToCreateRequest: return NSLocalizedString("Unable to create the URLRequest object", comment: "")
//
}
}
#1
현지화된 에러메세지를 띄우기 위한 RestManager의 사용자 지정 에러를 extension으로 선언해줘야합니다.
그 에러 메세지는 문자열 타입으로 받을 것입니다.
Switch case 문을 보시면 아시겠지만 .failedToCreateRequest case에서는 URL리퀘스트 객체를 생성할 수 없다는 오류 메세지를 띄워줄 것입니다.
Extension 전체 코드입니다.🙂
extension RestManager {
enum HttpMethod: String {
case get
case post
case put
case patch
case delete
}
struct RestEntity {
private var values: [String: String] = [:]
mutating func add(
value: String,
forKey key: String) {
values[key] = value
}
func value(forKey key: String) -> String? {
return values[key]
}
func allValues() -> [String: String] {
return values
}
func totalItems() -> Int {
return values.count
}
}
/// Response
struct Response {
var httpStatusCode: Int = 0
var headers = RestEntity()
var response: URLResponse?
init(fromURLResponse response: URLResponse?) {
guard let response = response else { return }
self.response = response
httpStatusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
if let headerFields = (response as? HTTPURLResponse)?.allHeaderFields {
for (key, value) in headerFields {
headers.add(
value: "\(value)",
forKey: "\(key)"
)
}
}
}
}
///Error case
enum CustomError: Error {
case failedToCreateRequest
}
///Result
struct Results {
var data: Data?
var response: Response?
var error: Error?
init(
withData data: Data?,
response: Response?,
error: Error?
) {
self.data = data
self.response = response
self.error = error
}
init(withError error: Error) {
self.error = error
}
}
}
extension RestManager.CustomError: LocalizedError {
public var localizedDescription: String {
switch self {
case .failedToCreateRequest: return NSLocalizedString("Unable to create the URLRequest object", comment: "")
}
}
}
지금까지 요청의 구조에 맞게 구조체를 생성했다면 이제부터는 여러 가지 기능을 추가하고자 합니다.
먼저, Extension 위에 class RestManager를 선언해줍니다.
class RestManager {
}
이 class 안에는 아래 5가지 기능이 필요합니다.
- URL쿼리 파라미터를 만드는 일
- 리퀘스트를 만드는 일
- 데이터를 얻는 일
- 특정 http method의 리퀘스트를 HTTPBody를 사용하여 전달하는 일
- 리퀘스트를 보내는 데에 필요한 준비 단계
그렇기 때문에 첫 번째로 리퀘스트에 필요한 requestHttpHeaders, urlQueryParameters, httpBodyParameters, httpBody를 위에서 선언해둔 RestEntity 구조체를 사용해야합니다.
var requestHttpHeaders = RestEntity()
var urlQueryParameters = RestEntity()
var httpBodyParameters = RestEntity()
var httpBody: Data?
그 다음으로 할 일은 리스폰스(응답)에 필요한 기능들을 하나씩 살펴보겠습니다.
첫 번째는 쿼리 파라미터를 추가하는 함수를 만들어보겠습니다.
// #1
private func addURLQueryParameters(toURL url: URL) -> URL {
// #2
if urlQueryParameters.totalItems() > 0 {
guard var urlComponents = URLComponents(
url: url,
resolvingAgainstBaseURL: false
) else { return url }
// #3
var queryItems = [URLQueryItem]()
for (key, value) in urlQueryParameters.allValues() {
let item = URLQueryItem(name: key, value: value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
)
queryItems.append(item)
}
urlComponents.queryItems = queryItems
guard let updatedURL = urlComponents.url else { return url }
return updatedURL
}
return url
}
#1
URL에 쿼리 파라미터를 추가하여 url로 리턴해주는 함수입니다.
#2
추가할 URL 쿼리 매개 변수가 있는지 확인해야 합니다.
만일 urlQueryParameters의 모든 value 총 갯수가 0보다 크면 swift에서 지원하는 URLComponents 구조체를 활용하여 url을 구성하는데 nil을 가질 수 있기 때문에 언래핑해줍니다.
이때 resolvingAgainstBaseURL은 우리가 파싱하기 전에 기본 url에 대해 분석해야하는 여부를 나타내는 이니셜라이저입니다.
만약 totalItems의 갯수가 0이면 url만 리턴시켜주면 되겠죠?
#3
URLQueryItem 메소드를 queryItems 배열로 생성해줍니다.
URLParameters에 있는 모든 value 값들을 for문을 돌려서 item이라는 상수에 띄어쓰기 값을 %로 변환한 value와 name을 담아줍니다. 그러고 난 다음에 선언해둔 queryItems 배열에 item을 추가하면 됩니다.
urlComponets의 queryItems(Swift 자체 지원해주는 프로퍼티)은 위에서 배열로 선언해둔 queryItems로 할당시킵니다.
그래서 쿼리 매개변수가 추가된 url이 없을 수도 있으므로 언래핑하여 가져오는데 추가된 url이 있으면 updateURL로 리턴시키고, 값이 없으면 'url만 리턴해줘라'라고 알려줍니다.
자, 이렇게 쿼리 파라미터를 추가하는 기능은 끝났습니다😉
// #1
private func getHttpBody() -> Data? {
// #2
guard let contentType = requestHttpHeaders.value(forKey: "Content-Type") else { return nil }
// #3
if contentType.contains("application/json") {
return try? JSONSerialization.data(
withJSONObject: httpBodyParameters.allValues(),
options: [.prettyPrinted, .sortedKeys]
)
} else if contentType.contains("application/x-www-form-urlencoded") {
let bodyString = httpBodyParameters.allValues().map {
"\($0)=\(String(describing: $1.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)))"
}.joined(separator: "&")
return bodyString.data(using: .utf8)
} else {
return httpBody
}
}
#1
POST, PUT 및 PATCH 리퀘스트는 HTTPBody를 사용하여 필요한 데이터를 보내야 합니다. 따라서 swift에서 제공하는 Data라는 구조체를 통해 리턴시키는 getHttpBody라는 함수를 만들어봅시다🥳
#2
우리는 앞에서 RestEntity라는 구조체를 requestHttpHeaders로 선언해두었죠?
그 안에 있는 "content-Type"인 키를 찾으면 해당 키의 value 값을 찾아서 리턴하고, 없으면 nil을 리턴시켜줍니다. (값이 없을 수도 있기 때문에 언래핑으로 해주는거 아시죠?)
http header란?
→ 헤더는 클라이언트와 서버 모두에 정보를 제공하는 데에 사용됩니다.
→ 인증 및 본문 내용에 대한 정보 제공 등 다양한 용도로 사용할 수 있습니다.
→ http header는 콜론으로 구분된 키-값 쌍으로 이루어져 있습니다.
→ curl를 사용하여 http header를 보낼 수 있습니다.
#3
만약 contentType이 "application/json"를 포함하고 있으면 즉, "application/json"이 true라면 'JSONSerialization.data를 리턴해라'라는 말입니다.
return try? JSONSerialization.data(
withJSONObject: httpBodyParameters.allValues(),
options: [.prettyPrinted, .sortedKeys]
)
우리는 httpBodyParameter' 개체를 JSON으로 변환해야 합니다.
이때, try? 를 쓰는 것은 에러 발생 시 nil을 반환해야하기 때문입니다. 에러가 발생하지 않으면 반환타입은 옵셔널입니다.
withJSONObject: httpBodyParameters,
JSONSerialization를 사용하여 JSON을 Foundation 개체로 변환하고 Foundation개체를 JSON으로 변환한다.
options: [.prettyPrinted, .sortedKeys]→
가독성을 위해 쓰이고, 공백 들여쓰기 prettyPrinted 타입 프로퍼티와
사전에 나와있는 정렬 상태로 만들어주는 sortedKeys 타입 프로퍼티를 사용합니다.
} else if contentType.contains("application/x-www-form-urlencoded") {
let bodyString = httpBodyParameters.allValues().map {
"\($0)=\(String(describing: $1.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)))"
}.joined(separator: "&")
return bodyString.data(using: .utf8)
} else {
return httpBody
}
아니면 contentType이 "application/x-www-form-urlencoded"이 포함되어있다면
httpBodyParameters 객체에 저장된 모든 key와 value로 이루어진 딕셔너리를 bodyString에 아래 코드처럼 mapping 해줍니다.
"\($0)=\(String(describing: $1.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)))"
0번째 인자는 문자열로만 되어있는 상태여야 합니다. 또 1번째 인자는 urlQueryAllowed 형식에 맞춰 문자열로 인코딩합니다.
}.joined(separator: "&")
return bodyString.data(using: .utf8)
} else {
return httpBody
}
}
}
그리고 "&"로 구분하여 배열의 원소들을 하나로 합쳐서(joined)
bodyString의 담긴 문자열을 utf8 인코딩을 사용하여 데이터로 리턴합니다.
utf8은 아스키 기반 시스템에 의한 전송이나 저장에 적합한 유니코드문자의 8 비트 표현입니다.
그게 아니라면 data형식의 httpBody를 리턴해줍니다.
자, 여기까지 POST, PUT 및 PATCH 리퀘스트는 HTTPBody를 사용하여 필요한 데이터를 보내는 함수를 만들어보았습니다!
이번엔 Request하는 데에 있어 필요한 준비 단계를 만들어보겠습니다.
private func prepareRequest(
// #1
withURL url: URL?,
httpBody: Data?,
httpMethod: HttpMethod
// #2
) -> URLRequest? {
guard let url = url else { return nil }
var request = URLRequest(url: url)
request.httpMethod = httpMethod.rawValue
// #3
for (header, value) in requestHttpHeaders.allValues() {
request.setValue(value, forHTTPHeaderField: header)
}
request.httpBody = httpBody
return request
}
#1
withURL url에는 URL 주소를,
Data를 가져올 httpBody를,
Http Method(get, post, put, patch, delete 인지 구분하기 위해)를 구분해줄 httpMethod를!
#2
#1에서 지정해둔 것들을 URLRequest로 리턴합니다.
그런데 그 URL Request가 nil일 수 있으니 옵셔널로 해줘야합니다.
// 1)
guard let url = url else { return nil }
// 2)
var request = URLRequest(url: url)
또 1) url은 옵셔널을 받고 있어 언래핑해주는 작업이 필요하고, 2)그 url을 인스턴스화 해주는 과정이 필요합니다!
request.httpMethod = httpMethod.rawValue
해당 httpMethod의 값을 사용하려면 rawValue라는 인스턴스 프로퍼티를 사용해줘야합니다.
URLrequest의 인스턴스 프로퍼티인 httpMethod에 httpMethod.rawValue 값을 넣어줍니다.
#3
request.httpBody = httpBody
return request
requestHttpHeaders의 모든 딕셔너리 값을 순회하면서 request에 setValue라는 메소드를 이용해 넣어줍니다. 또 우리가 위에서 data로 지정해둔 http body를 request에 있는 인스턴스 프로퍼티인 httpBody에 넣어주는 일을 해야합니다.
setValue → 헤더필드의 값을 설정해주는 메소드
value: 헤더 필드의 새 값, field: 설정할 헤더 필드의 이름
이렇게 prepareRequest 함수를 만들어보았습니다😃
이번엔 서버로 리퀘스트를 보내는 makeRequest 함수를 만들어볼까요?
func makeRequest(
// #1
toURL url: URL,
withHttpMethod httpMethod: HttpMethod,
completion: @escaping (_ result: Results) -> Void
) {
// #2
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
let targetURL = self?.addURLQueryParameters(toURL: url)
let httpBody = self?.getHttpBody()
guard let request = self?.prepareRequest(
withURL: targetURL,
httpBody: httpBody,
httpMethod: httpMethod) else {
completion(
Results(withError: CustomError.failedToCreateRequest)
)
return
}
// #3
let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)
let task = session.dataTask(with: request) { (data, response, error) in
completion(
Results(
withData: data,
response: Response(fromURLResponse: response),
error: error)
)
}
task.resume()
}
}
#1
url, httpMethod, completion(요청 결과)에 대한 메소드를 생성해줍니다!
completion시 데이터를 반환하거나 실패 시, 0을 반환하는 completion handler입니다.
Escaping Closure의 특징
- 클로저가 함수로부터
Escape
한다는 것은 해당 함수의 인자로 클로저가 전달되지만, 함수가 반환된 후 실행되는 것을 의미합니다. - 함수의 인자가 함수의 영역을 탈출하여 함수 밖에서 사용할 수 있는 개념은 기존에 우리가 알고 있던 변수의
scope
개념을 무시합니다. - 함수에서 선언된 로컬 변수가 로컬 변수의 영역을 뛰어넘어 함수 밖에서도 유효하기 때문입니다.
#2
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
(약한 참조로) 순차적으로 들어온 task들을 유저가 터치한 것을 우선으로 비동기 작업을 해줍니다.
→ qos level : userInitiated 이란? 터치하는 것을 우선순위로 올려서 앱의 성능을 좋게 합니다.
→ enum구조로 된 Qos(quality of service)class는 DispatchQueue에서 수행할 작업을 분류합니다.
let targetURL = self?.addURLQueryParameters(toURL: url)
targetURL 상수에 우리가 앞에서 만든 addURLQueryParameters function의 toURL값을 makeRequest에서 생성한 메소드의 url로 지정합니다.
let httpBody = self?.getHttpBody()
guard let request = self?.prepareRequest(
withURL: targetURL,
httpBody: httpBody,
httpMethod: httpMethod) else {
completion(
Results(withError: CustomError.failedToCreateRequest)
)
return
}
httpBody라는 상수에 getHttpBody에 있는 데이터를 담습니다.
request라는 상수에 prepareRequest function에서 URLRequest부분을 담아 언래핑해줍니다. (nil이 될 수 있기 때문에!)
withURL은 targetURL을 담고, httpBody는 httpBody를 담고, httpMethod는 httpMethod를 담습니다.
하지만 nil이라면 completion을 수행하여 에러를 빠져나와야 합니다. 우리가 이미 위에서 열거 형태의 사용자 지정 에러를 만들었죠?
이제 makeRequest 함수의 마지막 부분을 살펴보겠습니다.
#3
let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)
let task = session.dataTask(with: request) { (data, response, error) in
completion(
Results(
withData: data,
response: Response(fromURLResponse: response),
error: error)
)
}
task.resume()
sessionConfiguration라는 상수에 URLSessionConfiguration을 통해 URLSession을 생성합니다.
이때 .default란? 본 통신을 할 때 사용되는 기본 세션 구성 객체입니다. (쿠키와 같은 저장 객체를 사용합니다.)
또 session이라는 상수에 URLSession 메소드에 내장되어있는 configuration을 위에서 생성해둔 sessionConfiguration로 담아줍니다.
task라는 상수에는 #2에서 언래핑하여 담아둔 request를 dataTask 메소드를 사용하여 보내줍니다.
dataTask는 NSData 객체들을 이용해 특정 URL로부터 정보를 주고 받기 위해 사용됩니다.
completion 구문을 살펴보면 간단하죠? Results안에 데이터들을 리턴하는 것 뿐입니다👀
마지막으로 가장 중요한 부분인데요!
task.resume()
}
}
바로 이 부분입니다⚡️
resume이라는 메소드를 사용해 '작업이 일시 중단된 경우, 다시 시작해라' 라고 구현해줍니다.
왜냐하면 새로 초기화 된 작업은 일시 중단된 상태에서 시작되므로 꼭 resume 메소드를 호출하여 작업을 다시 시작해야하기 때문입니다.
자 이렇게 makeRequest 부분의 마지막까지 잘 살펴봤는데요! 이제 진짜 마지막 함수만 만들면 됩니다😉
url 자체에서 데이터를 가져오는 getData 함수를 만들어보고자 하는데요!
makeRequest에서 다루던 것들을 getData 함수에서도 다루기 때문에 조금 더 수월할 듯 합니다.
// #1
func getData(
fromURL url: URL,
completion: @escaping (_ data: Data?) -> Void
) {
// #2
DispatchQueue.global(qos: .userInitiated).async {
let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)
let task = session.dataTask(with: url) { (data, response, error) in
guard let data = data else { completion(nil); return }
completion(data)
}
task.resume()
}
}
}
#1
데이터를 가져올 URL,
요청 결과를 알려줄 completion까지!
completion시 데이터를 반환하거나 실패 시, 0을 반환하는 completion handler입니다.
#2
DispatchQueue.global(qos: .userInitiated).async {
네, 이것도 역시 순차적으로 들어온 task들을 유저가 터치한 것을 우선으로 비동기 작업을 해줍니다.
let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)
let task = session.dataTask(with: url) { (data, response, error) in
sessionConfiguration라는 상수에 URLSessionConfiguration을 통해 URLSession을 생성합니다.
이때 .default란? 본 통신을 할 때 사용되는 기본 세션 구성 객체입니다. (쿠키와 같은 저장 객체를 사용합니다.)
또 session이라는 상수에 URLSession 메소드에 내장되어있는 configuration을 위에서 생성해둔 sessionConfiguration로 담아줍니다.
task라는 상수에는 데이터가 있냐 없냐만 필요하기 때문에 dataTask를 통해 그 여부를 확인해줍니다.
let task = session.dataTask(with: url) { (data, response, error) in
guard let data = data else { completion(nil); return }
completion(data)
}
데이터를 가져왔는 지 여부를 언래핑하여 확인한 다음,
데이터가 있으면 실제 데이터를 리턴하고 값이 없다면 nil을 전달하는 completion handler를 써줍니다.
값이 있다면 data를 담으면 되겠죠?
그러고나서 아까와 마찬가지로 resume을 해주면 됩니다😂
task.resume()
무슨 작업이라고 했죠? 네 맞습니다.
'작업이 일시 중단된 경우, 다시 시작해라' 라고 구현해줍니다.
(새로 초기화 된 작업은 일시 중단된 상태에서 시작되기 때문에!)
자 이렇게 class RestManager도 모두 다 살펴봤습니다.
class RestManager 코드 전체입니다🙂
class RestManager {
/// Request
var requestHttpHeaders = RestEntity()
var urlQueryParameters = RestEntity()
var httpBodyParameters = RestEntity()
/// Optional
var httpBody: Data?
private func addURLQueryParameters(toURL url: URL) -> URL {
if urlQueryParameters.totalItems() > 0 {
guard var urlComponents = URLComponents(
url: url,
resolvingAgainstBaseURL: false
) else { return url }
var queryItems = [URLQueryItem]()
for (key, value) in urlQueryParameters.allValues() {
let item = URLQueryItem(name: key, value: value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed))
queryItems.append(item)
}
urlComponents.queryItems = queryItems
guard let updatedURL = urlComponents.url else { return url }
return updatedURL
}
return url
}
private func getHttpBody() -> Data? {
guard let contentType = requestHttpHeaders.value(forKey: "Content-Type") else { return nil }
if contentType.contains("application/json") {
return try? JSONSerialization.data(
withJSONObject: httpBodyParameters.allValues(),
options: [.prettyPrinted, .sortedKeys]
)
} else if contentType.contains("application/x-www-form-urlencoded") {
let bodyString = httpBodyParameters.allValues().map {
"\($0)=\(String(describing: $1.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)))"
}.joined(separator: "&")
return bodyString.data(using: .utf8)
} else {
return httpBody
}
}
private func prepareRequest(
withURL url: URL?,
httpBody: Data?,
httpMethod: HttpMethod
) -> URLRequest? {
guard let url = url else { return nil }
var request = URLRequest(url: url)
request.httpMethod = httpMethod.rawValue
for (header, value) in requestHttpHeaders.allValues() {
request.setValue(value, forHTTPHeaderField: header)
}
request.httpBody = httpBody
return request
}
func makeRequest(
toURL url: URL,
withHttpMethod httpMethod: HttpMethod,
completion: @escaping (_ result: Results) -> Void
) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
let targetURL = self?.addURLQueryParameters(toURL: url)
let httpBody = self?.getHttpBody()
guard let request = self?.prepareRequest(
withURL: targetURL,
httpBody: httpBody,
httpMethod: httpMethod) else {
completion(
Results(withError: CustomError.failedToCreateRequest)
)
return
}
let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)
let task = session.dataTask(with: request) { (data, response, error) in
completion(
Results(
withData: data,
response: Response(fromURLResponse: response),
error: error)
)
}
task.resume()
}
}
func getData(
fromURL url: URL,
completion: @escaping (_ data: Data?) -> Void
) {
DispatchQueue.global(qos: .userInitiated).async {
let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)
let task = session.dataTask(with: url) { (data, response, error) in
guard let data = data else { completion(nil); return }
completion(data)
}
task.resume()
}
}
}
Rest API 전체 코드입니다.
class RestManager {
/// Request
var requestHttpHeaders = RestEntity()
var urlQueryParameters = RestEntity()
var httpBodyParameters = RestEntity()
/// Optional
var httpBody: Data?
private func addURLQueryParameters(toURL url: URL) -> URL {
if urlQueryParameters.totalItems() > 0 {
guard var urlComponents = URLComponents(
url: url,
resolvingAgainstBaseURL: false
) else { return url }
var queryItems = [URLQueryItem]()
for (key, value) in urlQueryParameters.allValues() {
let item = URLQueryItem(name: key, value: value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed))
queryItems.append(item)
}
urlComponents.queryItems = queryItems
guard let updatedURL = urlComponents.url else { return url }
return updatedURL
}
return url
}
private func getHttpBody() -> Data? {
guard let contentType = requestHttpHeaders.value(forKey: "Content-Type") else { return nil }
if contentType.contains("application/json") {
return try? JSONSerialization.data(
withJSONObject: httpBodyParameters.allValues(),
options: [.prettyPrinted, .sortedKeys]
)
} else if contentType.contains("application/x-www-form-urlencoded") {
let bodyString = httpBodyParameters.allValues().map {
"\($0)=\(String(describing: $1.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)))"
}.joined(separator: "&")
return bodyString.data(using: .utf8)
} else {
return httpBody
}
}
private func prepareRequest(
withURL url: URL?,
httpBody: Data?,
httpMethod: HttpMethod
) -> URLRequest? {
guard let url = url else { return nil }
var request = URLRequest(url: url)
request.httpMethod = httpMethod.rawValue
for (header, value) in requestHttpHeaders.allValues() {
request.setValue(value, forHTTPHeaderField: header)
}
request.httpBody = httpBody
return request
}
func makeRequest(
toURL url: URL,
withHttpMethod httpMethod: HttpMethod,
completion: @escaping (_ result: Results) -> Void
) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
let targetURL = self?.addURLQueryParameters(toURL: url)
let httpBody = self?.getHttpBody()
guard let request = self?.prepareRequest(
withURL: targetURL,
httpBody: httpBody,
httpMethod: httpMethod) else {
completion(
Results(withError: CustomError.failedToCreateRequest)
)
return
}
let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)
let task = session.dataTask(with: request) { (data, response, error) in
completion(
Results(
withData: data,
response: Response(fromURLResponse: response),
error: error)
)
}
task.resume()
}
}
func getData(
fromURL url: URL,
completion: @escaping (_ data: Data?) -> Void
) {
DispatchQueue.global(qos: .userInitiated).async {
let sessionConfiguration = URLSessionConfiguration.default
let session = URLSession(configuration: sessionConfiguration)
let task = session.dataTask(with: url) { (data, response, error) in
guard let data = data else { completion(nil); return }
completion(data)
}
task.resume()
}
}
}
extension RestManager {
enum HttpMethod: String {
case get
case post
case put
case patch
case delete
}
struct RestEntity {
private var values: [String: String] = [:]
mutating func add(
value: String,
forKey key: String) {
values[key] = value
}
func value(forKey key: String) -> String? {
return values[key]
}
func allValues() -> [String: String] {
return values
}
func totalItems() -> Int {
return values.count
}
}
/// Response
struct Response {
var httpStatusCode: Int = 0
var headers = RestEntity()
var response: URLResponse?
init(fromURLResponse response: URLResponse?) {
guard let response = response else { return }
self.response = response
httpStatusCode = (response as? HTTPURLResponse)?.statusCode ?? 0
if let headerFields = (response as? HTTPURLResponse)?.allHeaderFields {
for (key, value) in headerFields {
headers.add(
value: "\(value)",
forKey: "\(key)"
)
}
}
}
}
///Error case
enum CustomError: Error {
case failedToCreateRequest
}
///Result
struct Results {
var data: Data?
var response: Response?
var error: Error?
init(
withData data: Data?,
response: Response?,
error: Error?
) {
self.data = data
self.response = response
self.error = error
}
init(withError error: Error) {
self.error = error
}
}
}
extension RestManager.CustomError: LocalizedError {
public var localizedDescription: String {
switch self {
case .failedToCreateRequest: return NSLocalizedString("Unable to create the URLRequest object", comment: "")
}
}
}