【Swift】UserDefaultsをもう少しちゃんと理解する


「UserDefaults消したーーーーい」と思って調べるとこのようなコードと出会った。

let appDomain = Bundle.main.bundleIdentifier
UserDefaults.standard.removePersistentDomain(forName: appDomain!)

なんとなくBundle Identifierを取ってきて指定すると消せるんだなというのはわかるが、UserDefaultsを消すときにBundle Identifierが必要なのかとか色々考えてしまったのでUserDefaultsについて調べてみました。

バージョン

Xcode: v11.3
Swift: v.5.3.2
iOS: v14.4

対象読者

  • iOSアプリ開発初心者

  • UserDefaultsをなんとなく使っている人

内容

UserDefaultsとは

UserDefaultsはユーザーごとの設定など保存するために提供されたKey-Valueストアです。また、UserDefaultsに保存された値は、削除しない限り永続的に保存され続けます。

ご存知ではあると思いますが、ユーザー個人のデータを保存する簡易的なデータベースみたいな感じですね。

Bundle IdentifierとUserDefaultsの関係は?

本題です。

結論から言うと、UserDefaultsを介して保存されるアプリ毎の設定を格納するファイル名にBundle Identifierが使われるという関係です。つまり、UserDefaultsはデータの保存領域で、Bundle Identifierはその領域の識別子として使われるという関係です。

つまりどういう事?

ここからはコードを使って説明します。

UserDefaultsは初めて書き込みが行われた時にアプリのRoot/Library/PreferencesBundle Identifier名.plistというファイルをデバイス内に生成します。

まず、アプリのRoot/Library/PreferencesBundle Identifier名.plistが生成されていないことを確認しましょう。

Library/PreferencesまでのパスはFileManagerを使うことで取得できます。

ViewController.swift
class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        getFileNamesFromPreferences() // 何も出力されない
    }

    func getFileNamesFromPreferences() {

        // Libraryまでのファイルパスを取得
        let filePath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]

        // filePathにPreferencesを追加
        let preferences = filePath.appendingPathComponent("Preferences")

        // Library/Preferences内のファイルのパスを取得
        guard let fileNames = try? FileManager.default.contentsOfDirectory(at: preferences, includingPropertiesForKeys: nil) else {
            return
        }

        // Library/Preferences内のファイル名を出力
        fileNames.compactMap { fileName in
            print(fileName.lastPathComponent)
        }
    }
}

Bundle Identifier名.plistを生成してみる

UserDefaultsに値を保存することでBundle Identifier名.plistが生成されるか実際に確認してみましょう。

ViewController.swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        getFileNamesFromPreferences() // 何も出力されない
+       UserDefaults.standard.set("Value1", forKey: "test") // UserDefaultsに値を保存
+       getFileNamesFromPreferences() // com.test.UserDefaults.plist
    }

    func getFileNamesFromPreferences() {

        // Libraryまでのファイルパスを取得
        let filePath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]

        // filePathにPreferencesを追加
        let preferences = filePath.appendingPathComponent("Preferences")

        // Library/Preferences内のファイルのパスを取得
        guard let fileNames = try? FileManager.default.contentsOfDirectory(at: preferences, includingPropertiesForKeys: nil) else {
            return
        }
        // Library/Preferences内のファイル名を出力
        fileNames.compactMap { fileName in
            print(fileName.lastPathComponent)
        }
    }
}

お手元で実行してみるとプロジェクトのBundle Identifier名.plistが保存されていることがわかると思います。

ちなみにpreferencesの値を使ってターミナルからも確認できます。

removePersistentDomain(forName:)

UserDefaultsに書き込みを行った際に生成される.plistファイルはBundle Identifier名でドメイン(簡単に言うと領域, 区画みたいなやつ)として認識されるみたいで、ドメイン名を指定することでドメイン内の値をすべて消すことができるみたいです。

ViewController.swift
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        getFileNamesFromPreferences() // 何も出力されない
        UserDefaults.standard.set("Value1", forKey: "test") // UserDefaultsに値を保存
        getFileNamesFromPreferences() // com.test.UserDefaults.plist
+       UserDefaults.standard.removePersistentDomain(forName: "com.test.UserDefaults.plist") // ドメイン内の値を全て削除
+       getFileNamesFromPreferences() // com.test.UserDefaults.plist
    }

    func getFileNamesFromPreferences() {

        // Libraryまでのファイルパスを取得
        let filePath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask)[0]

        // filePathにPreferencesを追加
        let preferences = filePath.appendingPathComponent("Preferences")

        // Library/Preferences内のファイルのパスを取得
        guard let fileNames = try? FileManager.default.contentsOfDirectory(at: preferences, includingPropertiesForKeys: nil) else {
            return
        }
        // Library/Preferences内のファイル名を出力
        fileNames.compactMap { fileName in
            print(fileName.lastPathComponent)
        }
    }
}

実行結果からわかるように、.plistファイル自体は削除されないみたいです。

終わりに

今回はUserDefaultsの内部構造に踏み込んでみました。

iOSの勉強は初めてからずっと心にあったモヤモヤが解消されてよかったです。

みなさんの参考になれば幸いです。

参考文献

swift - アプリ内で保存したファイルの一覧を取得したい - スタック・オーバーフロー

Swift5でDocumentディレクトリのファイルにアクセスする - Qiita

【Swift】URLでパスを扱うときに便利なプロパティとメソッド - Qiita

UserDefaults under the hood 💁🏻‍ | by Aaina jain | Swift India | Medium