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