# iOSで外部ストレージへの書き込みエラーを判定する

外部ストレージやファイルプロバイダへの書き込み時に発生したエラーを判定する実装です。

  • Cocoa の書き込みエラーを判定します。
  • 未接続デバイスや I/O エラーなどの POSIX エラーを判定します。
  • NSUnderlyingErrorKey をたどり、内部エラーも再帰的に確認します。

# 用語

  • 外部ストレージ: USB ドライブや、Files アプリから提供されるクラウドストレージなど、アプリの通常の保存領域の外にあるファイル保存先です。
  • Error: Swift でエラーを表す共通のプロトコルです。
  • NSError: Apple のエラー情報を、domaincode、追加情報を持つオブジェクトとして扱う型です。Swift の ErrorNSError に変換すると、エラーの種類を詳しく判定できます。
  • userInfo: NSError が持つ追加情報の辞書です。失敗したパスや内部エラーなどが格納されます。
  • エラードメイン: エラーを発生元ごとに分類する文字列です。同じ数値のエラーコードでも、ドメインが異なれば意味が変わります。
  • NSCocoaErrorDomain: ファイル操作など、Apple の Cocoa フレームワークが返すエラーのドメインです。Cocoa は iOS や macOS のアプリ開発用フレームワーク群の総称です。
  • NSPOSIXErrorDomain: OS の低レベルなファイル・デバイス操作で使われる POSIX エラーのドメインです。POSIX は Unix 系 OS で共通化された API とエラーコードの規格です。
  • NSFileProviderErrorDomain: Files アプリやクラウドストレージ連携を担う File Provider が返すエラーのドメインです。File Provider は、外部の保存先をアプリ内のファイルのように扱えるようにする仕組みです。
  • NSUnderlyingErrorKey: 表面上のエラーの原因となった、より詳細な内部エラーを userInfo から取得するためのキーです。
  • 再帰的な確認: 内部エラーにもさらに内部エラーがある場合に、同じ判定処理を繰り返して原因をたどることです。

# 判定するエラーコード

# Cocoa の書き込みエラー

  • fileWriteUnknown: 詳細な原因を特定できない書き込み失敗です。
  • fileNoSuchFile: 書き込み先のファイルまたは親フォルダが存在しません。
  • fileWriteNoPermission: 保存先への書き込み権限がありません。
  • fileWriteOutOfSpace: 保存先の空き容量がありません。
  • fileWriteVolumeReadOnly: 保存先が読み取り専用です。

# POSIX のデバイス・入出力エラー

  • ENODEV: 対象のデバイスが存在しない、または取り外されています。
  • EIO: 読み書き中に入出力(I/O)エラーが発生しました。I/O は Input/Output の略です。
  • ENOENT: 指定したファイルまたはフォルダが存在しません。
  • ENXIO: デバイスや接続先が利用できません。
  • ESTALE: ファイル参照が古くなり、現在の保存先を指せなくなっています。ネットワークストレージや外部ストレージの再接続後に発生することがあります。

rawValueCocoaError のケースを数値のエラーコードとして取り出す値です。Int(...) は POSIX の定数を NSError.code と比較できる整数へ変換しています。

    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"
    }