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