新しいエラーハンドリング
Swift 2でthrowを使ったエラーハンドリングが新たに導入された。従来のNSErrorを使ったエラーハンドリングの問題点は、メソッドにNSErrorポインタの代わりにnilを渡すことで無視できてしまうことだった。新たに導入されたエラーハンドリングでは、throwsキーワードが宣言されたメソッドを呼び出す際にdo-catch文で囲うことを強制される。throwで投げられるエラーはNSErrorではなくErrorTypeというprotocolを実装した値だ。Cocoaフレームワーク内のNSErrorを使っていたメソッドはthrowsを使うように置き換えられており、今後は独自のエラーを定義する場合はNSErrorではなくErrorTypeを使うのが望ましいと考えられる。しかし、ErrorTypeにも問題点はあり現実的な設計方針を検討する必要がある。
アプリ独自エラーの実装
NSErrorの代わりにErrorTypeを使っていく流れがあるものの、ErrorTypeにはNSErrorが持っていたlocalizedDescriptionやuserInfoといったエラー情報がないという問題点がある。そこで、ErrorTypeを継承した新たなprotocolを定義するという方針を考えてみた。
protocol FriendlyErrorType: ErrorType {
var summary: String { get }
var reason: String? { get }
var suggestion: String? { get }
}
このFriendlyErrorTypeを使って以下のように独自エラーを定義できる。
enum ApplicationError: FriendlyErrorType {
case SomethingWrong
case DecodeFailed([String])
var summary: String {
switch self {
case .SomethingWrong:
return "Something wrong"
case .DecodeFailed(_):
return "Decode failed"
}
}
var reason: String? {
switch self {
case .SomethingWrong:
return .None
case .DecodeFailed(let fields):
let failedFields = fields.joinWithSeparator(", ")
return "Failed to decode following fields: \(failedFields)"
}
}
var suggestion: String? {
switch self {
case .SomethingWrong:
return .None
case .DecodeFailed:
return .None
}
}
}
また、CocoaフレームワークのメソッドはErrorTypeを投げるようになったものの、Alamofire等のライブラリを使う際にはNSErrorを使うことになるため、FriendlyErrorTypeを実装するようにNSErrorを拡張する。
extension NSError: FriendlyErrorType {
var summary: String {
return localizedDescription
}
var reason: String? {
return userInfo[NSLocalizedFailureReasonErrorKey] as? String
}
var suggestion: String? {
return userInfo[NSLocalizedRecoverySuggestionErrorKey] as? String
}
}
なぜprotocol extensionではなく継承なのか
protocol extensionだとErrorTypeにデフォルトの実装を与えることになる。その場合、ErrorTypeとして渡されたエラーに対してメソッドを呼ぶと、すべてそのデフォルトの実装の結果が返るようになる。一方、FriendlyErrorTypeはただのprotocolなので、メソッドの結果はメソッドを実装する各クラスの結果を反映する。
extension ErrorType {
var summary: String {
return ""
}
}
extension NSError {
var summary: String {
return localizedDescription
}
}
let error: ErrorType = NSError(domain: "com.github.naoty.playground", code: 1000, userInfo: [NSLocalizedDescriptionKey: "Something wrong"])
print(error.summary) //=> "\n"
protocol FriendlyErrorType: ErrorType {
var summary: String { get }
}
extension NSError: FriendlyErrorType {
var summary: String {
return localizedDescription
}
}
let error: FriendlyErrorType = NSError(domain: "com.github.naoty.playground", code: 1000, userInfo: [NSLocalizedDescriptionKey: "Something wrong"])
print(error.summary) //=> "Something wrong\n"
エラーの利用例
FriendlyErrorTypeを実装したエラー型を実際に利用してみる。Alamofire、SwiftTask、Himotokiを使ってQiita APIを呼び出している。
return Task<Void, [Item], FriendlyErrorType> { progress, fulfill, reject, configure in
Alamofire.request(.GET, "https://qiita.com/api/v2/items").responseJSON { response in
switch response.status {
case .Success(let value):
if let objects = value as? [AnyObject] {
var items: [Item] = []
for object in objects {
do {
let item = try decode(object) as Item
items.append(item)
} catch DecodeError.MissingKeyPath(let keyPath) {
reject(ApplicationError.DecodeFailed(keyPath.components))
} catch {
reject(ApplicationError.SomethingWrong)
}
}
fulfill(items)
} else {
reject(ApplicationError.DecodeFailed(["root"]))
}
case .Failure(let error):
reject(error)
}
}
}
NSErrorを拡張しているため、ApplicationErrorとNSErrorをFriendlyErrorTypeとして並べて扱うことができている。
FriendlyErrorTypeを使ってアラートを表示する実装は以下のようなイメージだ。
let title = error.summary
var message = ""
if let reason = error.reason {
message += reason
message += "\n"
}
if let suggestion = error.suggestion {
message += suggestion
}
let alertController = UIAlertController(title: title, message: message, preferredStyle: .Alert)
presentViewController(alertController, animated: true, completion: nil)
以上のような方針に基づいたサンプルアプリケーションを用意した。