はじめに
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
に準拠するようになりますので、RawValue
がString
の場合時に、extension
でUserDefaultsKey
を拡張しメソッドの中身を定義しています。
また、キーが重複しないように'key'を追加して、クラス名と合わせて生成するようにしています。
以上、私が考えたUserDefaultの扱い方でした。