iT邦幫忙

2022 iThome 鐵人賽

DAY 9
0
Mobile Development

在 iOS 開發路上的大小事2系列 第 9

【在 iOS 開發路上的大小事2-Day09】將網路請求獨立成一個物件並透過 Generic 來將 API 網路請求改寫一下吧

  • 分享至 

  • xImage
  •  

一般在撰寫 API 網路請求的時候,可能會寫在需要請求的 Controller 裡面

但假如有好幾個 Controller 需要進行網路請求的話,那豈不是要寫好幾次長得很像的 Code 嗎

這樣不行,需要有更好的寫法才行!!!

那就撰寫一個專門處理網路請求的物件好了,就叫做 NetworkManager 了

然後一個 App 中,只會存在一個專門處理網路請求的物件
所以要使用 Singleton,來確保只有單一實例,像是下面這樣

Singleton

class NetworkManager: NSObject {
    
    static let shared = NetworkManager()
    
}

NetworkConstants

這裡就以常見的 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"
    }
}

func requestData()-1

接著,再回到 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

接著是處理 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
    }
}

func requestData()-2

處理完之後,再回傳給 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)
    }
    
}

func requestData()-3

接著就可以撰寫 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()
    }
}

NetworkManager 完整程式碼

將上面的各區塊組合起來,就會像下面這樣~

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)
    }
}

參考資料

  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

上一篇
【在 iOS 開發路上的大小事2-Day08】當個製圖大師-第三方套件 Charts 之圓餅圖
下一篇
【在 iOS 開發路上的大小事2-Day10】MVC vs MVVM!MVVM 是什麼?能吃嗎?(上)
系列文
在 iOS 開發路上的大小事230
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言