iT邦幫忙

2022 iThome 鐵人賽

DAY 16
1
Mobile Development

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

【在 iOS 開發路上的大小事2-Day16】非黑即白的 Result type

  • 分享至 

  • xImage
  •  

從 Swift 5 開始,導入了一種新型別,Result type
正如他的名字一樣,Result 就是結果的意思,白話一點就是 O 或 X 啦
而他是透過 enum 來定義的,讓我們來看一下

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 (泛型) 定義 SuccessFailure
其中 Failure 限制需遵守 Protocol Error
以及透過 associated value 來將資料儲存起來,方便後續使用

那 Result type 可以用來做什麼呢?

像是一般進行 API 呼叫時,常會用的 URLSession 就是一個很好的例子
我們先來看一下常見的 URLSession.shared.dataTask

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 來做示範

一般 API 的呼叫方式

我們先來看一下,不使用 Result type 時,API 該如何呼叫

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 後

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 呼叫的時候,該如何處理

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 呼叫

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

上一篇
【在 iOS 開發路上的大小事2-Day15】PhotoKit 好像很好玩 (4)
下一篇
【在 iOS 開發路上的大小事2-Day17】Application Extension 簡介
系列文
在 iOS 開發路上的大小事230
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言