Posted at

iOSのUserDefaultsの扱い方を考えてみる


はじめに

iOSのUserDefaultsは便利で、データを簡単に永続化できるのでよく利用しています。

おもにアプリの設定画面の状態や、アプリ全体に関わるフラグの管理などで利用していました。

UserDefaultsの便利な扱い方を考えてみたいと思います。


環境

動作を確認した環境は以下です。

xcode10

Swift4.2


基本的な使い方

まずはUserDefaultsの基本的な使い方を復習。

let key = "hogehoge"

UserDefaults.standard.set("fugafuga", forKey: key)
UserDefaults.standard.string(forKey: key)
UserDefaults.standard.removeObject(forKey: key)

後はキーを定数で複数用意すれば様々な情報を永続化できます。


keyの扱い方を考える

キーをString型の定数で扱ってましたが、誤って同じ文字列にしてしまうと問題になってしまいます。

そこで、enumを使ってkeyを定義してやれば重複するのを回避できます。

enum UserDefaultsKey: String {

case hogehoge
}
UserDefaults.standard.set("fugafuga", forKey: UserDefaultsKey.hogehoge.rawValue)
UserDefaults.standard.object(forKey: UserDefaultsKey.hogehoge.rawValue)
UserDefaults.standard.removeObject(forKey: UserDefaultsKey.hogehoge.rawValue)

これで、キーが重複することはなくなりました。

が、かなりコード量が増えてしまいましたので、UserDefaultsKeyにメソッドを追加してみました。

enum UserDefaultsKey: String {

case hogehoge

func get<T: Any>(def: T) -> T {
return UserDefaults.standard.object(forKey: self.rawValue) as? T ?? def
}

func set(value: Any) {
UserDefaults.standard.set(value, forKey: self.rawValue)
}

func remove() {
UserDefaults.standard.removeObject(forKey: self.rawValue)
}
}

UserDefaultsKey.hogehoge.set(value: "hugahuga")
UserDefaultsKey.hogehoge.get(def: "")
UserDefaultsKey.hogehoge.remove()

呼び出しがスッキリしました。


初期値について

UserDefaultsKey.get(def:)に取得できなかった場合の値を渡すようにしてましたが、呼び出し側で都度、値を渡すのも微妙です。

初期値については、決まっているはずなのでenum側で持たせるようにしてやったほうが良さそうです。

さらに、UserDefaultsには初期値を登録するregister()というメソッドも用意されているのでそちらも利用できるようなメソッドも作りました。

enum UserDefaultsKey: String, CaseIterable {

case hogehoge

var def: Any {
switch self {
case .hogehoge: return "hogehoge"
}
}

static func register() {
var defaults = [String: Any]()
for e in self.allCases {
defaults[e.rawValue] = e.def
}
UserDefaults.standard.register(defaults: defaults)
}

func get<T: Any>() -> T {
if !(self.def is T) {
fatalError("def and T are not the same type.")
}
return UserDefaults.standard.object(forKey: self.rawValue) as? T ?? self.def as! T
}

func set(value: Any) {
if type(of: self.def) != type(of: value) {
fatalError("value and def are not the same type.")
}
UserDefaults.standard.set(value, forKey: self.rawValue)
}

func remove() {
UserDefaults.standard.removeObject(forKey: self.rawValue)
}
}

UserDefaultsKey.register()
UserDefaultsKey.hogehoge.set(value: "hugahuga")
let hogehoge: String = UserDefaultsKey.hogehoge.get()
UserDefaultsKey.hogehoge.remove()

こんな感じです。

register()は、Swift4.2から追加されたCaseIterableを使って、enumの全要素にアクセスして、defを元に初期値を生成して登録しています。

初期値を渡していたget()も、defが追加したので引数はなしにしています。

その代わり、呼び出し側で戻り値の型を明確にする必要が出てきました。

let hogehoge: String = UserDefaultsKey.hogehoge.get()

また、初期値が出来たのでget()set()で初期値の型と一致しているかを、チェックするようにしています。


protocol化

一通りの機能としては出来たのですが、汎用性を考えてみたいと思います。

たとえば、設定画面用のキーとアプリ全体のキーを分けたいや、プライベートなキーを作りたい、他のプロジェクトでも利用したい等となった場合、上記のenumを都度作らないといけなくなります。

ということで、protocol化して汎用的に扱えるようにしてみました。

protocol UserDefaultsKey: CaseIterable {

var def: Any { get }
static func register()
func get<T: Any>() -> T
func set(value: Any)
func remove()
}

extension UserDefaultsKey where Self: RawRepresentable, Self.RawValue == String {
var key: String {
return "\(Self.self).\(self.rawValue)"
}

static func register() {
var defaults = [String: Any]()
for c in self.allCases {
defaults[c.rawValue] = c.def
}
UserDefaults.standard.register(defaults: defaults)
}

func get<T: Any>() -> T {
if !(self.def is T) {
fatalError("def and T are not the same type.")
}
return UserDefaults.standard.object(forKey: self.key) as? T ?? self.def as! T
}

func set(value: Any) {
if type(of: self.def) != type(of: value) {
fatalError("value and def are not the same type.")
}
UserDefaults.standard.set(value, forKey: self.key)
}

func remove() {
UserDefaults.standard.removeObject(forKey: self.key)
}
}

enum Settings: String, UserDefaultsKey {

case hogehoge

var def: Any {
switch self {
case .hogehoge: return "hogehoge"
}
}
}

Settings.register() // 初期値を登録
var hogehoge: String = Settings.hogehoge.get() // "hogehoge"が取得できる
Settings.hogehoge.set(value: "hugahuga")
hogehoge = Settings.hogehoge.get() // "hugahuga"が取得できる
Settings.hogehoge.remove()

RawRepresentableは、RawValueというassociatedtypeを持つプロトコルです。

enumは自動的にRawRepresentableに準拠するようになりますので、RawValueStringの場合時に、extensionUserDefaultsKeyを拡張しメソッドの中身を定義しています。

また、キーが重複しないように'key'を追加して、クラス名と合わせて生成するようにしています。

以上、私が考えたUserDefaultの扱い方でした。