⚠️
【iOS】APIのエラーハンドリング
エラーハンドリングの目標
- ユーザー体験の向上: エラーが発生しても、ユーザーが次に取るべき行動を明確にできる。
- デバッグ性の向上: 開発者がエラーの原因を特定しやすくする。
- システムの安定性の向上: エラーがシステム全体に波及することを防ぐ。
全体のフロー
- リクエストを発行: API クライアントがリクエストを送信。
- レスポンスを解析: ステータスコードとデータを解析して適切なエラー型に変換。
- ドメインエラーを変換: レスポンスエラーをサービス固有のエラーに変換。
- UIに通知: エラーをユーザーに通知し、必要に応じて再試行や操作案内を提示。
- ログを記録: エラー情報を収集し、デバッグまたは分析に役立てる。
構造
APIのエラーハンドリングを以下のレイヤーに分けて設計する
- データ層 (Network/API レベル)
- ドメイン層 (サービスレベル)
- プレゼンテーション層 (UI レベル)
今回扱うアプリ
1. データ層 (Network/API レベル)
- API呼び出しの結果を取得し、エラーを適切にラップして上位層に渡す。
- 通常は Result 型 (.success or .failure) やErrorプロトコルに準拠した型(カスタムエラー型とする)を使用。
Result型
Result 型は Swift 標準ライブラリで提供される型で、処理結果が 成功 か 失敗 のいずれかである場合に使用する。
// TechTrainAPIErrorについては次の例で定義した
func fetchData(completion: @escaping (Result<String, TechTrainAPIError>) -> Void) {
let success = false // 成功か失敗かをシミュレーション
if success {
// 成功時にデータをラップしてクロージャに渡す
completion(.success("データ取得成功"))
} else {
// 失敗時にサーバーエラーをラップしてクロージャに渡す
completion(.failure(.serverError(
statusCode: 403,
messageJP: "認証エラー",
messageEN: "You are not authorized user"
)))
}
}
カスタムエラー型
Result型を使って、エラー時にErrorプロトコルに準拠した型を扱うイメージ
TechTrainAPIError.swift
enum TechTrainAPIError: Error {
case invalidURL
case networkError
case serverError(statusCode: Int, messageJP: String, messageEN: String)
case decodingError
case keychainSaveError
case unknown
var localizedDescription: String {
switch self {
case .invalidURL:
return "無効なURLです。"
case .networkError:
return "ネットワークエラーが発生しました"
case .serverError(let statusCode, let messageJP, let messageEN):
return "サーバーエラーが発生しました (\(statusCode)):\nJP: \(messageJP)\nEN: \(messageEN)"
case .decodingError:
return "データの解読に失敗しました。"
case .keychainSaveError:
return "Keychain 保存エラーが発生しました"
case .unknown:
return "不明なエラーが発生しました。"
}
}
...続く
2. ドメイン層(サービスレベル)
- API クライアントやデータベースのエラーを集約してドメイン特有のエラーに変換。
- この層では、エラーのリカバリや再試行のロジックを記述することなどもある。
TechTrainAPIError.swift
...続き
/// サービス独自のエラー型
enum ServiceError: Error {
case invalidRequest(messageJP: String, messageEN: String)
case unauthorized(messageJP: String, messageEN: String)
case notFound(messageJP: String, messageEN: String)
case conflict(messageJP: String, messageEN: String)
case serviceUnavailable(messageJP: String, messageEN: String)
case serverIssue(messageJP: String, messageEN: String)
case underlyingError(TechTrainAPIError)
case unknown
var localizedDescription: String {
switch self {
case .invalidRequest(let messageJP, _):
return messageJP
case .unauthorized(let messageJP, _):
return messageJP
case .notFound(let messageJP, _):
return messageJP
case .conflict(let messageJP, _):
return messageJP
case .serviceUnavailable(let messageJP, _):
return messageJP
case .serverIssue(let messageJP, _):
return messageJP
case .underlyingError(let error):
return error.debugDescription
case .unknown:
return "不明なエラーが発生しました。"
}
}
}
// サービス層でエラーを細分化
func toServiceError() -> ServiceError {
switch self {
case .serverError(let statusCode, let messageJP, let messageEN):
switch statusCode {
case 400:
print("Service: バリデーションエラー - \(messageJP)")
return .invalidRequest(messageJP: messageJP, messageEN: messageEN)
case 401:
print("Service: 認証エラー - \(messageJP)")
return .unauthorized(messageJP: messageJP, messageEN: messageEN)
case 404:
print("Service: リソースが見つかりません - \(messageJP)")
return .notFound(messageJP: messageJP, messageEN: messageEN)
case 409:
print("Service: 競合エラー - \(messageJP)")
return .conflict(messageJP: messageJP, messageEN: messageEN)
case 503:
print("Service: サービス利用不可 - \(messageJP)")
return .serviceUnavailable(messageJP: messageJP, messageEN: messageEN)
default:
print("Service: サーバー側の問題 - \(messageJP)")
return .serverIssue(messageJP: messageJP, messageEN: messageEN)
}
default:
return .underlyingError(self)
}
}
}
サービス独自のエラーに変換してViewControllerなどで扱いやすくする
BookReviewService.swift
func postBookReview(
title: String,
url: String,
detail: String,
review: String,
token: String,
completion: @escaping (Result<Void, TechTrainAPIError.ServiceError>) -> Void
) {
let headers = ["Authorization": "Bearer \(token)"]
let endpoint = "/books"
let parameters: [String: Any] = [
"title": title,
"url": url,
"detail": detail,
"review": review
]
TechTrainAPIClient.shared.makeRequest(to: endpoint, method: "POST", parameters: parameters, headers: headers) { result in
switch result {
case .success:
completion(.success(()))
case .failure(let error):
// サービス独自のエラーに変換
completion(.failure(error.toServiceError()))
}
}
}
3. プレゼンテーション層 (UI レベル)
- ユーザーにエラーを視覚的に通知し、次のアクションを促す。
- UIAlertControllerやLabelなどを使ってエラーを表示。
EditBookReviewVC.swift
private func createReview() {
guard validateInputs(), let token = getToken() else { return }
BookReviewService.shared.postBookReview(
title: editView.titleTextField.text!,
url: editView.urlTextField.text!,
detail: editView.detailInputField.text!,
review: editView.reviewInputField.text!,
token: token
) { [weak self] result in
DispatchQueue.main.async {
switch result {
case .success:
self?.showAlert(title: "成功", message: "レビューが投稿されました", completion: {
self?.clearFields()
})
case .failure(let error):
// アラートでエラーの内容を表示
self?.showError(message: error.localizedDescription)
}
}
}
}
まとめ
適切にエラーを扱うことで、ユーザー体験の向上、デバッグ性の向上、そしてシステムの安定性を高めることができる。
Discussion