FriendlyErrorType
新しいエラーハンドリング
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)
以上のような方針に基づいたサンプルアプリケーションを用意した。