一般在撰寫 API 網路請求的時候,可能會寫在需要請求的 Controller 裡面
但假如有好幾個 Controller 需要進行網路請求的話,那豈不是要寫好幾次長得很像的 Code 嗎
這樣不行,需要有更好的寫法才行!!!
那就撰寫一個專門處理網路請求的物件好了,就叫做 NetworkManager 了
然後一個 App 中,只會存在一個專門處理網路請求的物件
所以要使用 Singleton,來確保只有單一實例,像是下面這樣
class NetworkManager: NSObject {
    
    static let shared = NetworkManager()
    
}
這裡就以常見的 RESTful API 來做設計
首先建立一個 NetworkConstants 的 struct 來宣告一些網路請求時會需要的參數
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"
    }
}
接著,再回到 NetworkManager~建立一個用來請求資料的 Function,並帶一些參數
像是請求方法,GET 還是 POST、API 路徑、Request 內容,並宣告一個 Closure 來將資料回傳出去
然後我們透過 Generic 定義 E、D
並限制 E 需遵守 Encodable Protocol、D 需遵守 Decodable Protocol
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 的部分~
這裡我們透過其他 Function 來做處理
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
    }
}
處理完之後,再回傳給 request 常數
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 了
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()
    }
}
將上面的各區塊組合起來,就會像下面這樣~
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)
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)
    }
}
- https://docs.swift.org/swift-book/LanguageGuide/Generics.html
 - https://developer.apple.com/documentation/foundation/urlsession
 - https://developer.apple.com/documentation/foundation/urlsession/1407613-datatask
 - https://developer.apple.com/documentation/foundation/urlrequest
 - https://developer.apple.com/documentation/foundation/urlcomponents
 - https://developer.apple.com/documentation/foundation/urlqueryitem
 - https://developer.apple.com/documentation/foundation/jsonserialization/
 - https://developer.apple.com/documentation/swift/result