###### tags: `第14屆IT邦鐵人賽文章`
# 【在 iOS 開發路上的大小事2-Day09】將網路請求獨立成一個物件並透過 Generic 來將 API 網路請求改寫一下吧
一般在撰寫 API 網路請求的時候,可能會寫在需要請求的 Controller 裡面
但假如有好幾個 Controller 需要進行網路請求的話,那豈不是要寫好幾次長得很像的 Code 嗎
這樣不行,需要有更好的寫法才行!!!
那就撰寫一個專門處理網路請求的物件好了,就叫做 NetworkManager 了
然後一個 App 中,只會存在一個專門處理網路請求的物件
所以要使用 [Singleton](https://zh.wikipedia.org/zh-tw/%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F),來確保只有單一實例,像是下面這樣
## Singleton
```swift
class NetworkManager: NSObject {
static let shared = NetworkManager()
}
```
## NetworkConstants
這裡就以常見的 RESTful API 來做設計
首先建立一個 NetworkConstants 的 struct 來宣告一些網路請求時會需要的參數
```swift
struct NetworkConstants {
static let baseURL = "https://"
enum HttpHeaderField: String {
case authentication = "Authorization"
case contentType = "Content-Type"
case acceptType = "Accept"
case acceptEncoding = "Accept-Encoding"
}
enum ContentType: String {
case json = "application/json"
case xml = "application/xml"
case x_www_form_urlencoded = "application/x-www-form-urlencoded"
}
enum HTTPMethod: String {
case options = "OPTIONS"
case get = "GET"
case head = "HEAD"
case post = "POST"
case put = "PUT"
case patch = "PATCH"
case delete = "DELETE"
case trace = "TRACE"
case connect = "CONNECT"
}
enum RequestError: Error {
case unknownError
case connectionError
case invalidResponse
case jsonDecodeFailed
case invalidRequest // statusCode 400
case authorizationError // statusCode 401
case notFound // statusCode 404
case internalError // statusCode 500
case serverError // statusCode 502
case serverUnavailable // statusCode 503
}
enum APIPathConstants: String {
case apiPathKey = "API_PATH_RAWVALUE"
}
}
```
## func requestData()-1
接著,再回到 NetworkManager~建立一個用來請求資料的 Function,並帶一些參數
像是請求方法,GET 還是 POST、API 路徑、Request 內容,並宣告一個 Closure 來將資料回傳出去
然後我們透過 Generic 定義 E、D
並限制 E 需遵守 Encodable Protocol、D 需遵守 Decodable Protocol
```swift
class NetworkManager: NSObject {
static let shared = NetworkManager()
func requestData<E: Encodable, D: Decodable>(httpMethod: NetworkConstants.HTTPMethod,
path: NetworkConstants.APIPathConstants,
parameters: E,
completion: @escaping (Result<D, Error>) -> Void) {
}
}
```
## 處理 URLRequest
接著是處理 URLRequest 的部分~
這裡我們透過其他 Function 來做處理
```swift
private func handleHTTPMethod<E: Encodable>(_ method: NetworkConstants.HTTPMethod,
_ path: NetworkConstants.APIPathConstants,
_ parameters: E) -> URLRequest {
let baseURL = NetworkConstants.baseURL
let url = URL(string: baseURL + path.rawValue)!
var urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
let httpType = NetworkConstants.ContentType.json.rawValue
urlRequest.allHTTPHeaderFields = [NetworkConstants.HttpHeaderField.contentType.rawValue : httpType]
urlRequest.httpMethod = method.rawValue
let dict1 = try? parameters.asDictionary()
switch method {
case .get:
let parameters = dict1 as? [String : String]
urlRequest.url = requestWithURL(urlString: urlRequest.url?.absoluteString ?? "", parameters: parameters ?? [:])
default:
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: dict1 ?? [:], options: .prettyPrinted)
}
return urlRequest
}
private func requestWithURL(urlString: String,
parameters: [String : String]?) -> URL? {
guard var urlComponents = URLComponents(string: urlString) else { return nil }
urlComponents.queryItems = []
parameters?.forEach { (key, value) in
urlComponents.queryItems?.append(URLQueryItem(name: key, value: value))
}
return urlComponents.url
}
extension Encodable {
func asDictionary() throws -> [String : Any] {
let data = try JSONEncoder().encode(self)
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String : Any] else {
throw NSError()
}
return dictionary
}
}
```
## func requestData()-2
處理完之後,再回傳給 request 常數
```swift
class NetworkManager: NSObject {
static let shared = NetworkManager()
func requestData<E: Encodable, D: Decodable>(httpMethod: NetworkConstants.HTTPMethod,
path: NetworkConstants.APIPathConstants,
parameters: E,
completion: @escaping (Result<D, Error>) -> Void) {
let urlRequest = handleHTTPMethod(httpMethod, path, parameters)
}
}
```
## func requestData()-3
接著就可以撰寫 URLSession 了
```swift
class NetworkManager: NSObject {
static let shared = NetworkManager()
func requestData<E: Encodable, D: Decodable>(httpMethod: NetworkConstants.HTTPMethod,
path: NetworkConstants.APIPathConstants,
parameters: E,
completion: @escaping (Result<D, Error>) -> Void) {
let urlRequest = handleHTTPMethod(httpMethod, path, parameters)
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {
completion(.failure(error))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200, let data = data else {
completion(.failure(error))
return
}
let decoder = JSONDecoder()
guard let results = try? decoder.decode(D.self, from: data) else {
completion(.failure(error))
return
}
completion(.success(results))
}.resume()
}
}
```
## NetworkManager 完整程式碼
將上面的各區塊組合起來,就會像下面這樣~
```swift
class NetworkManager: NSObject {
static let shared = NetworkManager()
func requestData<E: Encodable, D: Decodable>(httpMethod: NetworkConstants.HTTPMethod,
path: NetworkConstants.APIPathConstants,
parameters: E,
completion: @escaping (Result<D, Error>) -> Void) {
let urlRequest = handleHTTPMethod(httpMethod, path, parameters)
URLSession.shared.dataTask(with: urlRequest) { (data, response, error) in
guard error == nil else {
completion(.failure(error!))
return
}
guard let response = response as? HTTPURLResponse, response.statusCode == 200, let data = data else {
completion(.failure(error!))
return
}
let decoder = JSONDecoder()
guard let results = try? decoder.decode(D.self, from: data) else {
completion(.failure(error!))
return
}
completion(.success(results))
}.resume()
}
private func handleHTTPMethod<E: Encodable>(_ method: NetworkConstants.HTTPMethod,
_ path: NetworkConstants.APIPathConstants,
_ parameters: E) -> URLRequest {
let baseURL = NetworkConstants.baseURL
let url = URL(string: baseURL + path.rawValue)!
var urlRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 10)
let httpType = NetworkConstants.ContentType.json.rawValue
urlRequest.allHTTPHeaderFields = [NetworkConstants.HttpHeaderField.contentType.rawValue : httpType]
urlRequest.httpMethod = method.rawValue
let dict1 = try? parameters.asDictionary()
switch method {
case .get:
let parameters = dict1 as? [String : String]
urlRequest.url = requestWithURL(urlString: urlRequest.url?.absoluteString ?? "", parameters: parameters ?? [:])
default:
urlRequest.httpBody = try? JSONSerialization.data(withJSONObject: dict1 ?? [:], options: .prettyPrinted)
}
return urlRequest
}
private func requestWithURL(urlString: String,
parameters: [String : String]?) -> URL? {
guard var urlComponents = URLComponents(string: urlString) else { return nil }
urlComponents.queryItems = []
parameters?.forEach { (key, value) in
urlComponents.queryItems?.append(URLQueryItem(name: key, value: value))
}
return urlComponents.url
}
}
extension Encodable {
func asDictionary() throws -> [String : Any] {
let data = try JSONEncoder().encode(self)
guard let dictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String : Any] else {
throw NSError()
}
return dictionary
}
}
```
## 如何使用
在上面已經將 NetworkManager 透過 Generic 來改寫完成了
那接下來就是該如何使用了
在 NetworkManager 裡面,我們透過 Generic 定義了 D,並限制 D 需遵守 Decodable Protocol
而這邊呼叫的時候,我們就需要告訴說 D 實際上到底是什麼!
而下面的 ```Response``` 就是 D 真實的型別
**(Response 是自己定義用來接收 API Response 的 struct)**
```swift
NetworkManager.shared.requestData(city: city) { (result: Result<Response, Error>) in
switch result {
case .success(let results):
// 處理 API 回傳的 Data
case.failure(let error):
print(error.localizedDescription)
}
}
```
## 參考資料
> 1. https://docs.swift.org/swift-book/LanguageGuide/Generics.html
> 2. https://developer.apple.com/documentation/foundation/urlsession
> 3. https://developer.apple.com/documentation/foundation/urlsession/1407613-datatask
> 4. https://developer.apple.com/documentation/foundation/urlrequest
> 5. https://developer.apple.com/documentation/foundation/urlcomponents
> 6. https://developer.apple.com/documentation/foundation/urlqueryitem
> 7. https://developer.apple.com/documentation/foundation/jsonserialization/
> 8. https://developer.apple.com/documentation/swift/result