###### 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