# iOSで外部ストレージへの書き込みエラーを判定する
外部ストレージやFile Providerへの書き込みが失敗したとき、エラーが内部エラーに包まれていて種類を判定しにくい。
NSError のドメインとコードで判定し、NSUnderlyingErrorKey の内部エラーを再帰的にたどる。
# 判定はCocoa・POSIX・File Providerの3つのドメインで行う
実装は次の3種類のエラーを判定する。
| ドメイン | 判定するエラー |
|---|---|
NSCocoaErrorDomain | Cocoaの書き込みエラー |
NSPOSIXErrorDomain | 未接続デバイスやI/OエラーなどのPOSIXエラー |
NSFileProviderErrorDomain | File Providerが返すエラー全般 |
表面のエラーが該当しない場合は、NSUnderlyingErrorKey の内部エラーも再帰的に確認する。
# 判定に使う用語
- 外部ストレージ: USBドライブや、Filesアプリから提供されるクラウドストレージなど、アプリの通常の保存領域の外にある保存先。
Error: Swiftでエラーを表す共通のプロトコル。NSError: Appleのエラー情報を、domain、code、追加情報を持つオブジェクトとして扱う型。SwiftのErrorをNSErrorに変換すると、エラーの種類を詳しく判定できる。userInfo:NSErrorが持つ追加情報の辞書。失敗したパスや内部エラーなどが入る。- エラードメイン: エラーを発生元ごとに分類する文字列。同じ数値のエラーコードでも、ドメインが異なれば意味が変わる。
NSCocoaErrorDomain: ファイル操作など、AppleのCocoaフレームワークが返すエラーのドメイン。CocoaはiOSやmacOSのアプリ開発用フレームワーク群の総称。NSPOSIXErrorDomain: OSの低レベルなファイル・デバイス操作で使われるPOSIXエラーのドメイン。POSIXはUnix系OSで共通化されたAPIとエラーコードの規格。NSFileProviderErrorDomain: Filesアプリやクラウドストレージ連携を担うFile Providerが返すエラーのドメイン。File Providerは、外部の保存先をアプリ内のファイルのように扱う仕組み。NSUnderlyingErrorKey: 表面上のエラーの原因となった内部エラーを、userInfoから取り出すためのキー。- 再帰的な確認: 内部エラーにさらに内部エラーがある場合に、同じ判定を繰り返して原因をたどること。
# Cocoaの書き込みエラーは5つのコードを判定する
| コード | 意味 |
|---|---|
fileWriteUnknown | 詳細な原因を特定できない書き込み失敗 |
fileNoSuchFile | 書き込み先のファイルまたは親フォルダが存在しない |
fileWriteNoPermission | 保存先への書き込み権限がない |
fileWriteOutOfSpace | 保存先の空き容量がない |
fileWriteVolumeReadOnly | 保存先が読み取り専用 |
# POSIXのデバイス・入出力エラーは5つのコードを判定する
| コード | 意味 |
|---|---|
ENODEV | 対象のデバイスが存在しない、または取り外されている |
EIO | 読み書き中に入出力(I/O、Input/Output)エラーが発生した |
ENOENT | 指定したファイルまたはフォルダが存在しない |
ENXIO | デバイスや接続先が利用できない |
ESTALE | ファイル参照が古くなり、現在の保存先を指せない |
ESTALE は、ネットワークストレージや外部ストレージの再接続後に発生することがある。
# 判定関数と内部エラーの再帰的な確認
private func isExternalStorageWriteError(_ error: Error) -> Bool {
let nsError = error as NSError
if isFileWriteError(nsError) || isDisconnectedPOSIXError(nsError) || isFileProviderUnavailableError(nsError) {
return true
}
if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? Error {
return isExternalStorageWriteError(underlyingError)
}
return false
}
private func isFileWriteError(_ error: NSError) -> Bool {
guard error.domain == NSCocoaErrorDomain else {
return false
}
return error.code == CocoaError.fileWriteUnknown.rawValue
|| error.code == CocoaError.fileNoSuchFile.rawValue
|| error.code == CocoaError.fileWriteNoPermission.rawValue
|| error.code == CocoaError.fileWriteOutOfSpace.rawValue
|| error.code == CocoaError.fileWriteVolumeReadOnly.rawValue
}
private func isDisconnectedPOSIXError(_ error: NSError) -> Bool {
guard error.domain == NSPOSIXErrorDomain else {
return false
}
return error.code == Int(ENODEV)
|| error.code == Int(EIO)
|| error.code == Int(ENOENT)
|| error.code == Int(ENXIO)
|| error.code == Int(ESTALE)
}
private func isFileProviderUnavailableError(_ error: NSError) -> Bool {
error.domain == "NSFileProviderErrorDomain"
}
rawValue は、CocoaError のケースを数値のエラーコードとして取り出す値である。
Int(...) は、POSIXの定数を NSError.code と比較できる整数へ変換している。
File Providerのエラーは、コードを区別せずドメインだけで判定している。