UserDefaults を部分的に Codable でデコードする

Swift プログラミング

UserDefaults のデータの特定の部分を Codable を使って独自の型のインスタンスに読み込んだり、それを UserDefaults に書き戻す方法を調べてみました。


Swift では Codable を使って、JSON データやプロパティーリストのデータと型とを相互変換できるようになっています。

iOS アプリや macOS アプリの設定を保存するのに使う UserDefaults も内部でプロパティーリスト形式のデータを扱うものになっているので、プロパティーリストファイルに適合する型を作ってそのインスタンスを生成することができるのですが、UserDefaults の使い勝手の良さもあり、さまざまな情報が自動で格納されていたりすることもあって、適合する型を維持管理していくのも少し厄介です。

ただ、プロパティーリストのデータは、特別な準備をしなくても、部分的に構成要素を取り出して、その要素とそれが内包する部分に限って Codable による型との相互変換が行えるので、そのやり方を知っておくと便利かもしれません。

UserDefaults のデータ

たとえば、次のような UserDefaults のデータがあったとします。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>Target Applications</key>
	<array>
		<dict>
			<key>Bundle Identifier</key>
			<string>com.apple.mainstage3</string>
		</dict>
		<dict>
			<key>Bundle Identifier</key>
			<string>com.apple.FinalCut</string>
		</dict>
	</array>
	<key>NSWindow Frame NSSpellCheckerSubstitutionsPanel2</key>
	<string>692 407 425 137 0 0 2560 1415 </string>
</dict>
</plist>

今回はこのデータの元ファイルに当たるプロパティーリストファイル全体から型に変換するのではなく、ここから Target Applications をキーにした配列のところだけを Codable で変換できるようにしてみます。

適合する型の定義

今回は Target Applications キーの内容に適合する型を作りますけれど、その値は配列になっていて、配列自体は標準で Codable に対応しているため、今回はその中身に適合する型を定義します。

構造自体はとても簡単で、キーである "Bundle Identifier" に該当するプロパティーをひとつ、そのデータを格納するために String型 で用意します。

struct Application : Codable {

	var bundleIdentifier: String
}

private extension Application {

	enum CodingKeys : String, CodingKey {

		case bundleIdentifier = "Bundle Identifier"
	}
}

Codable による相互変換

ここまで準備ができたら、あとは UserDefaultsTarget Applications の内容と Application型 の配列とを相互変換可能になっています。

UserDefaults の一部を独自の型として読み込む

まずは UserDefaults に保存されている Target Applications の内容から [Application]型 を生成してみます。

func getTargetApplications() throws -> [Application] {

	let userDefaults = UserDefaults.standard

	guard let object = userDefaults.object(forKey: "Target Applications") else {
		fatalError()
	}

	let decoder = PropertyListDecoder()
	let data = try PropertyListSerialization.data(fromPropertyList: object, format: .xml, options: 0)

	return try decoder.decode([Applications].self, from: data)
}

処理の流れとしては、目的のキーに該当するデータをオブジェクトとして UserDefaults から取り出し、それをいったん PropertyListSerialization を使ってプロパティーリストの形に整えた上で、そのデータを Codable に対応した PropertyListDecoder を使って目的の型のインスタンスを生成しています。

独自のデータ型の値を UserDefaults に書き込む

書き込みも先ほどの処理と逆のような流れで作っていけます。

流れとしては、値を Codable でエンコードしたら、それをオブジェクトに変換して UserDefaults の目的のに書き込みます。

func setTargetApplications(_ applications: [Application]) throws {

	let userDefaults = UserDefaults.standard
	let encoder = PropertyListEncoder()

	let data = try encoder.encode(applications)
	let object = try PropertyListSerialization.propertyList(from: data, format: nil)

	userDefaults.set(object, forKey: "Target Applications")
}

このようにすることで [Application]型 の値を UserDefaultsTarget Applications に該当するところにプロパティーリストのデータとして書き戻すことができました。