💨

【翻訳】Taking Advantage of Swift's Native Result Type

2023/07/26に公開

アプリケーションを構築するために使用する Cocoa API のほとんどは、 Objective-C によって駆動されます。これは、これらの API を利用するために Objective-C を使用する必要があることを意味しませんが、API は、あなたが Swift API から期待するいくつかの利点を欠いていることを意味します。

例として URLSession API を見てみましょう。 リモート API にリクエストを送信することは、 URLSession API を使用して簡単です。実装は簡単ですが、気の利いた感じはしません。私が何を言いたいのかお見せしましょう。

URLSession APIを使う

Xcodeを起動し、iOS > Playground セクションから Blank テンプレート を選択して playground を作成します。 playground の名前を Networking とします。

playgroundの内容を削除し、UIKitフレームワークのimport文を追加します。

import UIKit

URLSession API を使ってリモートサーバーから画像を取得したい。 URL を作成し、 playground で作業しているので、感嘆符を使って初期化の結果を強制的にアンラップします。

import UIKit

// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!

画像のデータを取得するために、 URLSessionDataTask インスタンス を作成します。リモートリソースの URL と完了ハンドラ(クロージャ)を渡して、共有 URL セッションにデータタスクを依頼します。完了ハンドラは、データタスクが成功または失敗して完了したときに実行されます。

import UIKit

// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!

// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in

}

補完ハンドラは3つの引数を受け取ります。オプションの Data オブジェクト 、オプションの URLResponse オブジェクト、そしてオプションの Error オブジェクト です。この例では、Data オブジェクトと Error オブジェクトに興味があります。 UIImage インスタンス を作成するには、 Data オブジェクト を安全にアンラップし、それを使って UIImage インスタンス を作成します。

import UIKit

// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!

// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in
    if let data = data, let image = UIImage(data: data) {
        print(image)
    } else {

    }
}

これは悪くないように見えますが、まだ完了ハンドラーでエラーを処理する必要があります。ここで実装が厄介になります。完了ハンドラーに渡されるエラーはオプションの型です。理論的には、 Data オブジェクト と Error オブジェクトの両方が nil に等しい可能性があります。安全策をとるなら、完了ハンドラーの else 節 で Error オブジェクト を安全にアンラップする必要があります。

import UIKit

// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!

// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in
    if let data = data, let image = UIImage(data: data) {
        print(image)
    } else {
        if let error = error {
            print(error)
        } else {
            print("no data, no error")
        }
    }
}

Data オブジェクト と Error オブジェクト の両方が nil に等しい場合、未知のエラーとして、リモートサーバーから画像を取得できなかったことをアプリケーションに通知する必要があります。

ドキュメントによれば、このようなことは決して起こらないはずです。ドキュメントによると、リクエストに成功した場合、 Error オブジェクト は nil に等しくなります。リクエストが失敗した場合、 Data オブジェクト は nil に等しく、Error オブジェクト は nil には等しくありません。

このドキュメントを念頭に置いて、 Error オブジェクト を安全にアンラップするために else if 節 を使うことで、完了ハンドラの実装を更新することができます。これで問題ないように見えますが、まだ問題があります。私たちはコードスメル (https://qiita.com/kayamin/items/7814a2344d90e613757d) を導入してしまったのです。 if 文 に1つ以上の else if 節 が含まれている場合、最後のelse if 節 の後には else 節 を続けなければなりません。これは switch 文 の default 節 に似ています。 if 節 や else if 節 でキャッチできなかったシナリオを確実にキャッチすることで、防御的なプログラミングをしたい。

import UIKit

// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!

// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in
    if let data = data, let image = UIImage(data: data) {
        print(image)
    } else if let error = error {
        print(error)
    }
}

そして、振り出しに戻ります。

import UIKit

// Create URL
let url = URL(string: "https://goo.gl/wV9G4I")!

// Create Data Task
let dataTask = URLSession.shared.dataTask(with: url) { (data, _, error) in
    if let data = data, let image = UIImage(data: data) {
        print(image)
    } else if let error = error {
        print(error)
    } else {
        print("unknown error")
    }
}

Result 型の導入

Swift プログラミング言語が導入された直後、開発者たちはこの厄介な問題の解決策を考え出しました。私も、 Building a Weather Application From Scratch (https://cocoacasts.com/series/building-a-weather-application-from-scratch) で解決策を紹介しました。RootViewModel クラス の実装を見てみましょう。 fetchWeatherData(for:) メソッド で、root view モデルは URLSessionDataTask インスタンス を使用して Dark Sky API から天気データを取得します。完了ハンドラでは、 Error オブジェクト と Data オブジェクト を安全にアンラップします。両方が nil に等しい場合、 else 節 が実行されます。

private func fetchWeatherData(for location: Location) {
    // Initialize Weather Request
    let weatherRequest = WeatherRequest(baseUrl: WeatherService.authenticatedBaseUrl, location: location)

    // Create Data Task
    URLSession.shared.dataTask(with: weatherRequest.url) { [weak self] (data, response, error) in
        if let response = response as? HTTPURLResponse {
            print("Status Code: \(response.statusCode)")
        }

        DispatchQueue.main.async {
            if let error = error {
                print("Unable to Fetch Weather Data \(error)")

                // Weather Data Result
                let result: WeatherDataResult = .failure(.noWeatherDataAvailable)

                // Invoke Completion Handler
                self?.didFetchWeatherData?(result)
            } else if let data = data {
                // Initialize JSON Decoder
                let decoder = JSONDecoder()

                // Configure JSON Decoder
                decoder.dateDecodingStrategy = .secondsSince1970

                do {
                    // Decode JSON Response
                    let darkSkyResponse = try decoder.decode(DarkSkyResponse.self, from: data)

                    // Weather Data Result
                    let result: WeatherDataResult = .success(darkSkyResponse)

                    // Update User Defaults
                    UserDefaults.didFetchWeatherData = Date()

                    // Invoke Completion Handler
                    self?.didFetchWeatherData?(result)
                } catch {
                    print("Unable to Decode JSON Response \(error)")

                    // Weather Data Result
                    let result: WeatherDataResult = .failure(.noWeatherDataAvailable)

                    // Invoke Completion Handler
                    self?.didFetchWeatherData?(result)
                }
            } else {
                // Weather Data Result
                let result: WeatherDataResult = .failure(.noWeatherDataAvailable)

                // Invoke Completion Handler
                self?.didFetchWeatherData?(result)
            }
        }
    }.resume()
}

この実装で興味深いのは、 WeatherDataResult 型 を使っていることです。どのようなものかお見せしましょう。 WeatherDataResult 列挙型 は、成功と失敗の2つのケースを定義しています。成功の場合は、関連する WeatherData 型の値を定義します。失敗の場合は、WeatherDataError 型の値を定義します。

import UIKit

class RootViewModel: NSObject {

    // MARK: - Types

    enum WeatherDataResult {
        case success(WeatherData)
        case failure(WeatherDataError)
    }

    enum WeatherDataError: Error {
        case notAuthorizedToRequestLocation
        case failedToRequestLocation
        case noWeatherDataAvailable
    }

    ...

}

これは私たちが探していたソリューションであり、 Objective-C にはないソリューションです。WeatherDataResult 列挙型 は、気象データ・リクエストの結果を明確に伝えます。成功した場合は、WeatherData 型 の値が関連付けられます。失敗した場合は、 WeatherDataError 型の値が関連付けられます。第3のシナリオはありません。

Swiftのネイティブな Result 型 の紹介

私は Swift 5 が導入される前に、Building a Weather Application From Scratch を作成しました。 RootViewModel クラス が WeatherDataResult 型 を定義しているのはそのためです。 Swift 5 は Result型 を導入することで、全てをより簡単で一貫性のあるものにしました。WeatherDataResult 型 を Swift のネイティブな Result 型 に置き換えることで、 RootViewModel クラスをリファクタリングしてみましょう。

Swift の Result 型 を見てみることから始めましょう。これは成功と失敗の2つのケースを定義しています。また、 Error プロトコルを継承する必要のある Failure で、2 つのジェネリック型、 Success と Failure を定義しています。これは非常に賢い解決策です。 Result 列挙型 の成功ケースには Success 型、失敗ケースには Failure 型 が関連します。

/// A value that represents either a success or a failure, including an
/// associated value in each case.
public 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)

    ...
}

このことを念頭に置いて、 RootViewModel クラス で Result 型 を使用することができます。解決策は、 WeatherDataResult 列挙型 のおかげで思ったより簡単です。 WeatherDataResult という名前の typealias を定義します。 typealias の型は Result<WeatherData, WeatherDataError> です。

import UIKit

class RootViewModel: NSObject {

    // MARK: - Type Aliases

    typealias WeatherDataResult = Result<WeatherData, WeatherDataError>

    // MARK: - Types

    enum WeatherDataResult {
        case success(WeatherData)
        case failure(WeatherDataError)
    }

    enum WeatherDataError: Error {
        case notAuthorizedToRequestLocation
        case failedToRequestLocation
        case noWeatherDataAvailable
    }

    ...
}

他に必要な変更は、 WeatherDataResult 列挙型 を削除することだけです。

import UIKit

class RootViewModel: NSObject {

    // MARK: - Type Aliases

    typealias WeatherDataResult = Result<WeatherData, WeatherDataError>

    // MARK: - Types

    enum WeatherDataError: Error {
        case notAuthorizedToRequestLocation
        case failedToRequestLocation
        case noWeatherDataAvailable
    }

    ...
}

これで終わりです。私たちは、 WeatherDataResult 列挙型を Swift の Result 型に置き換えることに成功しました。 typealias を使用することで、 WeatherDataResult の全ての出現を Result<WeatherData, WeatherDataError> に置き換える必要はありません。 typealias の使用はオプションですが、可読性を向上させます。

次は?

Swift のネイティブの Result 列挙型の追加は、カスタムの Result 列挙型を定義する必要がなくなるので、最も歓迎されます。この解決策を強力にするのは、列挙型、ジェネリック、関連する値の組み合わせです。この組み合わせは、Objective-Cでは利用できず、いくつかの Objective-C の API が Swift のネイティブ API ほどエレガントに感じない理由です。

【翻訳元の記事】

Swift and Cocoa Essentials
Taking Advantage of Swift's Native Result Type
https://cocoacasts.com/swift-and-cocoa-fundamentals-taking-advantage-of-swifts-native-result-type

Discussion