# 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のエラーは、コードを区別せずドメインだけで判定している。