使用 URLSession 串接 http get，post，delete，put API，以 Reqres API 為例
By: 彼得潘的 Ios App Neverland
Tags: iOS, Xcode, Swift

開發 iOS App 時，我們時常需要跟網路上的後台(server) 溝通。如下圖所示，我們的 App 是 client，它會發送 request 給 server，然後 server 再回傳 response，比方將 JSON 格式的電影資料回傳給 App。

HTTP method

當 App 發送 request 給網路上的 server 時，目前最常見的方法是透過 http 或 https，而 http(https) 傳送的 request 有個重要的 method 欄位，可以說明 request 的目的為何。

比較常用的 http(https) method 有以下幾種:

GET: 取得資料。

POST: 建立資料。

DELETE: 刪除資料。

PUT / PATCH: 修改更新資料。

詳細的說明可參考以下連結。

使用 HTTP method 表示 request 目的

串接 API 有很多寫法，因為後台工程師可以自訂資料的格式和處理方式，比方他可以規定讀取，新增和刪除資料的 request 都是 post，透過上傳的 JSON 資料區分讀取，新增和刪除。

不過目前比較常見的主流是採用 REST API((RESTful API)，它可以讓我們串接的 API 有個統一的標準，使用 HTTP method 的 GET，POST，DELETE & PUT 區分讀取，新增，刪除和修改。想了解 REST API 的朋友，可進一步參考以下連結。

方便測試 API 的 reqres

為了示範如何使用 URLSession 串接 http get，post，delete，put API，接下來我們將以方便測試 API 的 reqres 為例說明。

Reqres API 提供豐富的 API，幫助我們方便測試 GET，POST，DELETE，PUT & PATCH。

抓取資料的 GET

reqres API 說明。

呼叫 dataTask(with:completionHandler:)，傳入 URL 即可產生搭配 http method GET 的 request 抓取資料。



URLSession.shared.dataTask(with: url) { (data, response, error) in

if let data = data,

let content = String(data: data, encoding: .utf8) {

print(content)

}

}.resume() let url = URL(string: " https://reqres.in/api/users?page=1 ")!URLSession.shared.dataTask(with: url) { (data, response, error) inif let data = data,let content = String(data: data, encoding: .utf8) {print(content)}.resume()

也可以呼叫 dataTask(with:completionHandler:)，傳入 URLRequest，它也會搭配 http method GET。



var request = URLRequest(url: url)

URLSession.shared.dataTask(with: request) { (data, response, error) in

if let data = data,

let content = String(data: data, encoding: .utf8) {

print(content)

}

}.resume() let url = URL(string: " https://reqres.in/api/users?page=1 ")!var request = URLRequest(url: url)URLSession.shared.dataTask(with: request) { (data, response, error) inif let data = data,let content = String(data: data, encoding: .utf8) {print(content)}.resume()

建立資料的 POST

reqres API 說明。

struct CreateUserBody: Encodable {

let name: String

let job: String

}

var request = URLRequest(url: url)

request.httpMethod = "POST"

request.setValue("application/json", forHTTPHeaderField: "Content-Type") let url = URL(string: " https://reqres.in/api/users ")!var request = URLRequest(url: url) let encoder = JSONEncoder()

let user = CreateUserBody(name: "Peter", job: "情歌王子")

let data = try? encoder.encode(user)

request.httpBody = data

URLSession.shared.dataTask(with: request) { (data, response, error) in

if let data = data,

let content = String(data: data, encoding: .utf8) {

print(content)

}

}.resume()

說明

request.httpMethod = "POST"

將 httpMethod 設為 POST。

request.setValue("application/json", forHTTPHeaderField: "Content-Type")

將 http header 的 Content-Type 設為 application/json，因為 reqres API 上傳的資料格式為 JSON。上傳的格式不一定會是 json，比方 application/x-www-form-urlencoded & multipart/form-data 也是常見的格式。

request.httpBody = data

從 URLRequest 的 httpBody 設定上傳的資料。

刪除資料的 DELETE

reqres API 說明。

let url = URL(string: "https://reqres.in/api/users/2")!

var request = URLRequest(url: url)

request.httpMethod = "DELETE"

URLSession.shared.dataTask(with: request) { (data, response, error) in

if let httpResponse = response as? HTTPURLResponse {

print(httpResponse.statusCode)

}



}.resume() var request = URLRequest(url: url)URLSession.shared.dataTask(with: request) { (data, response, error) inif let httpResponse = response as? HTTPURLResponse {}.resume()

說明

依據 reqres API 的說明，刪除的資料由 url 的 user id 決定。

request.httpMethod = "DELETE"

將 httpMethod 設為 DELETE。

print(httpResponse.statusCode)

依據 reqres API 的說明，刪除成功時 http status code 為 204。

更新資料的 PUT / PATCH

reqres API 說明。

struct UpdateUserBody: Encodable {

let name: String

let job: String

} let url = URL(string: "https://reqres.in/api/users/2")!

var request = URLRequest(url: url)

request.httpMethod = "PUT"

request.setValue("application/json", forHTTPHeaderField: "Content-Type") var request = URLRequest(url: url) let encoder = JSONEncoder()

let user = UpdateUserBody(name: "Peter", job: "情歌王子")

let data = try? encoder.encode(user)

request.httpBody = data

URLSession.shared.dataTask(with: request) { (data, response, error) in

if let data = data,

let content = String(data: data, encoding: .utf8) {

print(content)

}



}.resume()

說明

依據 reqres API 的說明，更新的資料由 url 裡的 user id 決定。

request.httpMethod = "PUT"

將 httpMethod 設為 PUT 或 PATCH。

request.setValue("application/json", forHTTPHeaderField: "Content-Type")

將 http header 的 Content-Type 設為 application/json，因為 reqres API 上傳的資料格式為 JSON。

request.httpBody = data

從 URLRequest 的 httpBody 設定上傳的資料。

搭配 Result Type 串接 reqres API

模仿 Apple 官方範例串接 reqres API，定義 function 型別的 completion 參數 & 使用 Result type

model 資料型別

struct UsersResponse: Decodable {

let page: Int

let perPage: Int

let total: Int

let totalPages: Int

let data: [User]

} struct User: Decodable {

let id: Int

let email: String

let firstName: String

let lastName: String

let avatar: URL

} struct CreateUserResponse: Decodable {

let name: String

let job: String

let id: String

let createdAt: Date

} struct UpdateUserResponse: Decodable {

let name: String

let job: String

let updatedAt: Date

} struct CreateUserBody: Encodable {

let name: String

let job: String

} struct UpdateUserBody: Encodable {

let name: String

let job: String

}

定義設定網址參數的 URL extension

extension URL {

func withQueries(_ queries: [String: String]) -> URL? {

var components = URLComponents(url: self, resolvingAgainstBaseURL: true)

components?.queryItems = queries.map {

URLQueryItem(name: $0.0, value: $0.1)

}

return components?.url

}

}

定義錯誤型別 NetworkError。

enum NetworkError: Error{ case invalidURL }

串接 API 的 NetworkController





static let shared = NetworkController()



let baseURL = URL(string: "



func getUsers(page: Int, completion:

let queries = ["page": "\(page)"]

guard let url = baseURL.appendingPathComponent("users").withQueries(queries) else {

completion(.failure(NetworkError.invalidURL))

return

}

URLSession.shared.dataTask(with: url) { (data, response, error) in

if let data = data {

do {

let decoder = JSONDecoder()

decoder.keyDecodingStrategy = .convertFromSnakeCase

let usersResponse = try decoder.decode(UsersResponse.self, from: data)

completion(.success(usersResponse))

} catch {

completion(.failure(error))

}

} else if let error = error {

completion(.failure(error))

}

}.resume()

}



func createUser(_ user: CreateUserBody, completion:

let url = baseURL.appendingPathComponent("users")

var request = URLRequest(url: url)

request.httpMethod = "POST"

request.setValue("application/json", forHTTPHeaderField: "Content-Type")



let encoder = JSONEncoder()

let data = try? encoder.encode(user)

request.httpBody = data

URLSession.shared.dataTask(with: request) { (data, response, error) in

if let data = data {

do {

let decoder = JSONDecoder()

let dateFormatter = ISO8601DateFormatter()

dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in

let container = try decoder.singleValueContainer()

let dateString = try container.decode(String.self)

if let date = dateFormatter.date(from: dateString) {

return date

} else {

throw DecodingError.dataCorruptedError(in: container, debugDescription: "")

}

})

let createUserResponse = try decoder.decode(CreateUserResponse.self, from: data)

completion(.success(createUserResponse))

} catch {

completion(.failure(error))

}

} else if let error = error {

completion(.failure(error))

}

}.resume()

}



func deleteUser(userId: Int, completion:

let url = baseURL.appendingPathComponent("users/\(userId)")

var request = URLRequest(url: url)

request.httpMethod = "DELETE"

URLSession.shared.dataTask(with: request) { (data, response, error) in

if let httpResponse = response as? HTTPURLResponse,

httpResponse.statusCode == 204 {

completion(.success("ok"))

} else if let error = error {

completion(.failure(error))

}

}.resume()

}



func updateUser(_ user: UpdateUserBody, completion:



let url = baseURL.appendingPathComponent("users")

var request = URLRequest(url: url)

request.httpMethod = "PUT"

request.setValue("application/json", forHTTPHeaderField: "Content-Type")



let encoder = JSONEncoder()

let data = try? encoder.encode(user)

request.httpBody = data

URLSession.shared.dataTask(with: request) { (data, response, error) in

if let data = data {

do {

let decoder = JSONDecoder()

let dateFormatter = ISO8601DateFormatter()

dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]

decoder.dateDecodingStrategy = .custom({ (decoder) -> Date in

let container = try decoder.singleValueContainer()

let dateString = try container.decode(String.self)

if let date = dateFormatter.date(from: dateString) {

return date

} else {

throw DecodingError.dataCorruptedError(in: container, debugDescription: "")

}

})

let updateUserResponse = try decoder.decode(UpdateUserResponse.self, from: data)

completion(.success(updateUserResponse))

} catch {

completion(.failure(error))

}

} else if let error = error {

completion(.failure(error))

}

}.resume()

}

} class NetworkController {static let shared = NetworkController()let baseURL = URL(string: " https://reqres.in/api ")!func getUsers(page: Int, completion: @escaping (Result ) -> Void) {let queries = ["page": "\(page)"]guard let url = baseURL.appendingPathComponent("users").withQueries(queries) else {completion(.failure(NetworkError.invalidURL))returnURLSession.shared.dataTask(with: url) { (data, response, error) inif let data = data {do {let decoder = JSONDecoder()decoder.keyDecodingStrategy = .convertFromSnakeCaselet usersResponse = try decoder.decode(UsersResponse.self, from: data)completion(.success(usersResponse))} catch {completion(.failure(error))} else if let error = error {completion(.failure(error))}.resume()func createUser(_ user: CreateUserBody, completion: @escaping (Result ) -> Void) {let url = baseURL.appendingPathComponent("users")var request = URLRequest(url: url)request.httpMethod = "POST"request.setValue("application/json", forHTTPHeaderField: "Content-Type")let encoder = JSONEncoder()let data = try? encoder.encode(user)request.httpBody = dataURLSession.shared.dataTask(with: request) { (data, response, error) inif let data = data {do {let decoder = JSONDecoder()let dateFormatter = ISO8601DateFormatter()dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]decoder.dateDecodingStrategy = .custom({ (decoder) -> Date inlet container = try decoder.singleValueContainer()let dateString = try container.decode(String.self)if let date = dateFormatter.date(from: dateString) {return date} else {throw DecodingError.dataCorruptedError(in: container, debugDescription: "")})let createUserResponse = try decoder.decode(CreateUserResponse.self, from: data)completion(.success(createUserResponse))} catch {completion(.failure(error))} else if let error = error {completion(.failure(error))}.resume()func deleteUser(userId: Int, completion: @escaping (Result ) -> Void) {let url = baseURL.appendingPathComponent("users/\(userId)")var request = URLRequest(url: url)request.httpMethod = "DELETE"URLSession.shared.dataTask(with: request) { (data, response, error) inif let httpResponse = response as? HTTPURLResponse,httpResponse.statusCode == 204 {completion(.success("ok"))} else if let error = error {completion(.failure(error))}.resume()func updateUser(_ user: UpdateUserBody, completion: @escaping (Result ) -> Void) {let url = baseURL.appendingPathComponent("users")var request = URLRequest(url: url)request.httpMethod = "PUT"request.setValue("application/json", forHTTPHeaderField: "Content-Type")let encoder = JSONEncoder()let data = try? encoder.encode(user)request.httpBody = dataURLSession.shared.dataTask(with: request) { (data, response, error) inif let data = data {do {let decoder = JSONDecoder()let dateFormatter = ISO8601DateFormatter()dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]decoder.dateDecodingStrategy = .custom({ (decoder) -> Date inlet container = try decoder.singleValueContainer()let dateString = try container.decode(String.self)if let date = dateFormatter.date(from: dateString) {return date} else {throw DecodingError.dataCorruptedError(in: container, debugDescription: "")})let updateUserResponse = try decoder.decode(UpdateUserResponse.self, from: data)completion(.success(updateUserResponse))} catch {completion(.failure(error))} else if let error = error {completion(.failure(error))}.resume()

串接 API

// http method get test

NetworkController.shared.getUsers(page: 1) { (result) in

switch result {

case .success(let usersResponse):

print(usersResponse)

case .failure(let error):

print(error)

}

} // http method post test

let createUserBody = CreateUserBody(name: "Peter", job: "Writer")

NetworkController.shared.createUser(createUserBody) { (result) in

switch result {

case .success(let createUserResponse):

print(createUserResponse)

case .failure(let error):

print(error)

}

} // http method delete test

NetworkController.shared.deleteUser(userId: 2) { (result) in

switch result {

case .success(_):

print("delete ok")

case .failure(let error):

print(error)

}

} // http method put test

let updateUserBody = UpdateUserBody(name: "Peter", job: "情歌王子")

NetworkController.shared.updateUser(updateUserBody) { (result) in

switch result {

case .success(let updateUserResponse):

print(updateUserResponse)

case .failure(let error):

print(error)

}

}

利用 capture list 解決 closure 可能產生的記憶體問題

呼叫 API 時，我們也可以在 closure 裡加上 capture list，它的好處可參考以下連結的說明。

精簡程式的 Generic 寫法

如果有很多 API，需要將 JSON 轉成多種不同的 Codable 型別，也可以將處理 reponse 的程式變成搭配 generic 的 function 來簡化程式。比方定義以下的 function handleResponse，利用 URLSession.shared.dataTask 得到資料後，只要呼叫 handleResponse 處理即可。(ps: 此方法只適合 JSONDecoder 解析的型別和 Result 成功時回傳的資料型別一致的 case，若不一樣需用另外的寫法。)





if let data = data {

do {

let response = try decoder.decode(T.self, from: data)

completion(.success(response))

} catch {

completion(.failure(error))

}

} else if let error = error {

completion(.failure(error))

}

} private func handleResponse (decoder: JSONDecoder, data: Data?, error: Error?, completion: @escaping (Result ) -> Void) where T: Decodable {if let data = data {do {let response = try decoder.decode(T.self, from: data)completion(.success(response))} catch {completion(.failure(error))} else if let error = error {completion(.failure(error))

let queries = ["page": "\(page)"]

guard let url = baseURL.appendingPathComponent("users").withQueries(queries) else {

completion(.failure(NetworkError.invalidURL))

return

}

URLSession.shared.dataTask(with: url) { (data, response, error) in

let decoder = JSONDecoder()

decoder.keyDecodingStrategy = .convertFromSnakeCase

self.handleResponse(decoder: decoder, data: data, error: error, completion: completion)



}.resume()

} func getUsers(page: Int, completion: @escaping (Result ) -> Void) {let queries = ["page": "\(page)"]guard let url = baseURL.appendingPathComponent("users").withQueries(queries) else {completion(.failure(NetworkError.invalidURL))returnURLSession.shared.dataTask(with: url) { (data, response, error) inlet decoder = JSONDecoder()decoder.keyDecodingStrategy = .convertFromSnakeCase}.resume()

其它方便練習的 API:

Swagger Petstore

Dummy api example

相關作業練習