【Swift備忘録】UserDefaultsに保存した画像をドキュメントディレクトリに移す作業

UserDefaultsには大容量のデータを保存してはいけないらしい

先日「こだまあるき」アプリのユーザーから「数日したら初日に戻る」という不具合報告があり、諸所確認してみると、画像をUserDefaultsに保存しているところが怪しい・・・ということがわかりました。
おそらくUserDefaultsには画像のような大容量のデータ保存をするのは非奨励らしく、画像はドキュメントディレクトリに保存した方がよいということみたいです。
今回の場合は55個の画像をdataで保存していたのでまあ確かに大容量と言われるとそうだなと・・。
(それでもテスト機等では特に不具合はないので、おそらく昔のiPhoneとかストレージがギリギリだと耐えられないとかかも)

じゃあ、これまでUserDefaultsに保存していたアプリをアップデートする際にどうすればいいかという流れをメモしておきます。

■STEP1 UserDefaultsに保存している画像データを取り出す

UserDefaultsに保存がある場合とない場合、ない場合は「初回」か「保存形式を変更後」かで分岐。
saveImagesToPNGFilesAndReturnFilePathsは次のStepで。

  if let imageDataArray = UserDefaults.standard.array(forKey: "KujiImage53") as? [NSData] {
            // 画像データの配列からファイルパスの配列を取得
            let filePaths = saveImagesToPNGFilesAndReturnFilePaths(imageArray: imageDataArray)
            
            // 別のキーにファイルパスの配列を保存
            UserDefaults.standard.set(filePaths, forKey: "ImageFilePaths53")
            
            // 元のキーからのデータを削除
            UserDefaults.standard.removeObject(forKey: "KujiImage53")
        } else {
            // 既存のデータが存在する場合の処理
            if let imageFilePaths = UserDefaults.standard.array(forKey: "ImageFilePaths53") as? [String], imageFilePaths.count == 55 {
                // 既存のデータが55個の画像パスを持つ場合、何もしない
            } else {
                // 既存のデータが不足している場合、新しい画像データの配列を生成して保存する
                let data = "1".data(using: .utf8)!
                let nsdata = NSData(data: data)
                var newImageDataArray: [NSData] = Array(repeating: nsdata, count: 55)
                
                // 画像データの配列からファイルパスの配列を取得
                let filePaths = saveImagesToPNGFilesAndReturnFilePaths(imageArray: newImageDataArray)
                
                // ファイルパスの配列をUserDefaultsに保存
                UserDefaults.standard.set(filePaths, forKey: "ImageFilePaths53")
            }
        }

■STEP2 画像をドキュメントディレクトリに保存する
次回以降ファイルパスで画像にアクセスするためにファイルパスの配列をUserDefaultsに保存することにしたが、これは後に不要なことが発覚!!その辺は後述。

// 画像データをPNG形式でファイルに保存してファイルパスを取得する関数
func saveImagesToPNGFilesAndReturnFilePaths(imageArray: [NSData]) -> [String] {
    var filePaths: [String] = []
    
    for (index, imageData) in imageArray.enumerated() {
        // Documentsディレクトリに画像を保存する
        guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            continue
        }
        let fileName = "image_\(index).png" // 画像ごとにファイル名を設定
        let fileURL = documentsDirectory.appendingPathComponent(fileName)
        
        // 画像データをPNG形式でファイルに書き込む
        do {
            try imageData.write(to: fileURL)
            filePaths.append(fileURL.path) // ファイルパスを配列に追加
        } catch {
           // print("Error saving image:", error)
        }
    }
    
    return filePaths
}
    

■STEP3 別途画像を上書きする場合
保存していたファイルパスをそのまま使うこともできる気もしたが、シュミレーターでアプリをアップデートした時などにファイルパスが変わる時があったので、ドキュメントにアクセスするファイルパスは毎回取得して、指定の画像名にアクセスするカタチにした。

まずはファイルパスを取得するメソッドを作る。今回は55個までの画像で「image_1.png」とかの名前と決まっているので・・

    //ドキュメントパス
    func getDocumentFilePath(forIndex index: Int) -> String? {
        guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            return nil
        }
        let fileName = "image_\(index).png"
        return documentsDirectory.appendingPathComponent(fileName).path
    }

で、保存する時はファイルパスを取得して、上書きする。

//ドキュメントディレクトリに画像の保存 
if let filePath = getDocumentFilePath(forIndex: Point) {
                       // 新しい画像をファイルに保存
                       do {
                           try imageData.write(to: URL(fileURLWithPath: filePath))
 
                       } catch {
                           //保存失敗時
                       }
                   } else {
                       // ファイルパスが取得できなかった場合のエラーハンドリング
                   }

■STEP4 画像の読み込み

保存時と同様に画像のファイルパスを生成して、アクセスする

    if let filePath = getDocumentFilePath(forIndex: sender.tag) {
                // ここで filePath を使用して画像にアクセスする
                animeimage.image = UIImage(contentsOfFile: filePath)
                
            } else {
                // ファイルパスが取得できなかった場合のエラーハンドリング
            }

■備考
途中でファイルパスが変わってしまう可能性に気づき、変更したがアップデートでファイルパスが変わらないなら、ファイルパスを配列にしてUserDefaultsに保存というカタチでいいと思う。

※参考 UserDefaultsに画像を保存する時は一度dataにして・・


            //画像をUserDefaultsに保存
   let data = image.pngData() as NSData?
   if let imageData = data {    
      imageArray.append(imageData)
       UserDefaults.standard.set(imageArray, forKey: "Image")     
   }

  if UserDefaults.standard.object(forKey: "Image") != nil {
      let objects = UserDefaults.standard.object(forKey: "Image") as? NSArray
      //配列としてUserDefaultsに保存した時の値と処理後の値が変わってしまうのでremoveAll()
      imageArray.removeAll()
      for data in objects! {
        imageArray.append(data as! NSData)
      }
   }

本件は以上です。
これでも他のアプリでもいくつかの画像をUserDefaultsに保存しちゃってるんで・・そのうち直さないといけないかも・・という恐怖が・・。