###### tags: `第14屆IT邦鐵人賽文章` # 【在 iOS 開發路上的大小事2-Day16】非黑即白的 Result type 從 Swift 5 開始,導入了一種新型別,Result type 正如他的名字一樣,Result 就是結果的意思,白話一點就是 O 或 X 啦 而他是透過 enum 來定義的,讓我們來看一下 ```swift enum Result<Success, Failure> where Failure : Error { /// A success, storing a `Success` value. case success(Success) /// A failure, storing a `Failure` value. case failure(Failure) } ``` 可以看到透過 Generic (泛型) 定義 ```Success``` 跟 ```Failure``` 其中 ```Failure``` 限制需遵守 Protocol Error 以及透過 associated value 來將資料儲存起來,方便後續使用 那 Result type 可以用來做什麼呢? 像是一般進行 API 呼叫時,常會用的 URLSession 就是一個很好的例子 我們先來看一下常見的 URLSession.shared.dataTask ```swift open func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask open func dataTask(with url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask ``` 可以看到 completionHandler 內定義了 ```(Data?, URLResponse?, Error?) -> Void``` 並不能透過 throws 將 API 呼叫中所遇到的錯誤丟出來 (當然啦,你也可以透過寫一個 failure 的 closure 來處理) 這時就可以透過 Result type 來進行改寫了! 這裡我以 [OpenWeatherAPI](https://github.com/leoho0722/OpenWeatherAPI) 來做示範 ## 一般 API 的呼叫方式 我們先來看一下,不使用 Result type 時,API 該如何呼叫 ```swift enum WeatherDataFetchError: Error { case invalidURL case requestFailed case responseFailed case jsonDecodeFailed } func getWeatherData(city: String, completion: @escaping (CurrentWeatherData?, WeatherDataFetchError?) -> Void) { let address = "https://api.openweathermap.org/data/2.5/weather?" let apikey = "YOUR_API_KEY" guard let url = URL(string: address + "q=\(city)" + "&appid=" + apikey) else { completion(nil, .invalidURL) return } URLSession.shared.dataTask(with: url) { (data, response, error) in guard error == nil else { completion(nil, .requestFailed) return } guard let response = response as? HTTPURLResponse, response.statusCode == 200, let data = data else { completion(nil, .responseFailed) return } let decoder = JSONDecoder() guard let weatherData = try? decoder.decode(CurrentWeatherData.self, from: data) else { completion(nil, .jsonDecodeFailed) return } completion(weatherData, nil) }.resume() } ``` 看起來好像還好,沒有哪邊特別的,跟平常寫的差不多啊 那我們先看一下改用 Result type 後的寫法 ## 改用 Result type 後 ```swift enum WeatherDataFetchError: Error { case invalidURL case requestFailed case responseFailed case jsonDecodeFailed } func getWeatherData(city: String, completion: @escaping (Result<CurrentWeatherData, WeatherDataFetchError>) -> Void) { let address = "https://api.openweathermap.org/data/2.5/weather?" let apikey = "YOUR_API_KEY" guard let url = URL(string: address + "q=\(city)" + "&appid=" + apikey) else { completion(.failure(.invalidURL)) return } URLSession.shared.dataTask(with: url) { (data, response, error) in guard error == nil else { completion(.failure(.requestFailed)) return } guard let response = response as? HTTPURLResponse, response.statusCode == 200, let data = data else { completion(.failure(.responseFailed)) return } let decoder = JSONDecoder() guard let weatherData = try? decoder.decode(CurrentWeatherData.self, from: data) else { completion(.failure(.jsonDecodeFailed)) return } completion(.success(weatherData)) }.resume() } ``` 雖然看起來感覺感覺沒什麼差,也沒有減少行數,那是不是繼續用原先的寫法就好啦 其實還是有差別的!像是原先透過 Closure 的寫法,也是可以,但會有個地方怪怪的 就是 CurrentWeatherData (天氣資料) 跟 WeatherDataFetchError (API 呼叫中遇到的錯誤) 這兩個結果應該只會出現一種,而不會出現像下面這個表格的狀況 | CurrentWeatherData | WeatherDataFetchError | |:------------------:|:---------------------:| | O | X | | X | O | | O | O | | X | X | 這時候就是 Result type 出現的時候了 ## Result type 特性 * 可以將非同步程式執行中所遇到的錯誤回傳出來 * 以更安全的方式處理錯誤 * 提高程式可讀以及更容易維護 * 不會有模稜兩可的狀態,只有 Success 跟 Failure 兩種狀態 ## 實際應用 上面是在實際 API 在執行的地方,那呼叫的地方又該如何寫呢 一樣,我們先看一般 API 呼叫的時候,該如何處理 ```swift WeatherAPIService.shared.getWeatherData(city: city) { weatherData, weatherFetchError in if weatherFetchError != nil { switch weatherFetchError { case .invalidURL: print("無效的 URL") case .requestFailed: print("Request Error") case .responseFailed: print("Response Error") case .jsonDecodeFailed: print("JSON Decode 失敗") case .none: break } } else { DispatchQueue.main.async { // 對 UI 進行對應處理 } } } ``` 改用 Result type 後的 API 呼叫 ```swift WeatherAPIService.shared.getWeatherData(city: city) { result in switch result { case .success(let weatherData): DispatchQueue.main.async { // 對 UI 進行對應處理 } case.failure(let fetchError): switch fetchError { case .invalidURL: print("無效的 URL") case .requestFailed: print("Request Error") case .responseFailed: print("Response Error") case .jsonDecodeFailed: print("JSON Decode 失敗") } } } ``` 可以看到改用 Result type 後,程式整體來說可讀性更好了! ## 參考資料 > 1. https://developer.apple.com/documentation/swift/result > 2. https://medium.com/%E5%BD%BC%E5%BE%97%E6%BD%98%E7%9A%84-swift-ios-app-%E9%96%8B%E7%99%BC%E5%95%8F%E9%A1%8C%E8%A7%A3%E7%AD%94%E9%9B%86/%E6%88%90%E5%8A%9F%E5%92%8C%E5%A4%B1%E6%95%97%E4%BA%8C%E6%93%87%E4%B8%80%E7%9A%84-result-type-e234c6fccc9c > 3. https://medium.com/@god913106/ios-swift-5-%E6%96%B0%E5%8A%9F%E8%83%BD-result-type-3f6d6528865b > 4. https://juejin.cn/post/6844903805184638990 > 5. https://medium.com/%E5%BD%BC%E5%BE%97%E6%BD%98%E7%9A%84-swift-ios-app-%E9%96%8B%E7%99%BC%E5%95%8F%E9%A1%8C%E8%A7%A3%E7%AD%94%E9%9B%86/enum-%E5%84%B2%E5%AD%98%E7%9B%B8%E9%97%9C%E8%81%AF%E8%B3%87%E6%96%99%E7%9A%84-associated-value-26ab3e061a16