0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

アプリケーションを構築するために使用する 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 です。

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 に置き換える必要はありません。 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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?