はじめに
iOS エンジニアのしだです。年始から喉が痛くずっとガラガラ声だったり左目が腫れたり2018年はなんだか嫌な予感がします。
最近、Kotlin を勉強しています。もう ことりん
という響きだけでいとおしいく感じてます。
Kotlin 勉強しているときに以下の記事を見まして便利だなぁと思ったので、iOS の UserDefaults
を同じように扱いたいなと思って書いてみました。
今回は iOS の UserDefaults の小ネタです。
準備
- Xcode 9.1 (Swift 4)
UserDefaults
iOS の UserDefaults
は、アプリ内で使える Key-Value ストアで、値は Property List として保存されます。
Android でいうところの SharedPreferences
です。
UserDefaults のみ
普段 UserDefaults
を使うときは、なにも考えずに以下のように書いてました。
struct Preference { static let userDefault = UserDefaults.standard struct Key { static let FirstLaunchedAt = "first_launched_at" } } extension Preference { static var firstLaunchedAt: Date? { get { return userDefault.object(forKey: Key.FirstLaunchedAt) as? Date } set { userDefault.set(newValue, forKey: Key.FirstLaunchedAt) userDefault.synchronize() } } } Preference.firstLaunchedAt = Date() Preference.firstLaunchedAt! // => 2018-01-07 05:03:57 +0000
保存する値が 多くなると extension Preference
に似たようなコードが増えて醜いなと感じていました。
UserDefaults + Generic Subscripts
Swift 4 から Generic Subscripts で subscript
に Generic がサポートされたのでより簡潔に書けます。
ちょっと Android の SharedPreferences
を意識して defaultValue
も付けてみました。
extension UserDefaults { subscript<T: Any>(key: String, defaultValue: T) -> T { get { let value = object(forKey: key) return (value as? T) ?? defaultValue } set { set(newValue, forKey: key) synchronize() } } subscript<T: Any>(key: String) -> T? { get { let value = object(forKey: key) return value as? T } set { guard let newValue = newValue else { removeObject(forKey: key) return } set(newValue, forKey: key) synchronize() } } } let userDefaults = UserDefaults.standard userDefaults["first_launched_at"] = Date() userDefaults["first_launched_at", Date()] // => 2018-01-07 05:20:37 +0000 let date: Date? = userDefaults["first_launched_at"] // => 2018-01-07 05:20:37 +0000
UserDefaults の Key を Enum で定義
enum
列挙型 といっしょに使えばもう少しわかりやすくなります。
enum Key: String { case firstLaunchedAt = "first_launched_at" } extension UserDefaults { subscript<T: Any>(key: Key, defaultValue: T) -> T { get { return self[key.rawValue, defaultValue] } set { self[key.rawValue] = newValue } } subscript<T: Any>(key: Key) -> T? { get { return self[key.rawValue] } set { self[key.rawValue] = newValue } } func remove(key: Key) { removeObject(forKey: key.rawValue) } } let userDefaults = UserDefaults.standard userDefaults[.firstLaunchedAt] = Date() userDefaults[.firstLaunchedAt, Date()] // => 2018-01-07 05:32:28 +0000 let date: Date? = userDefaults[.firstLaunchedAt] // => 2018-01-07 05:32:28 +0000
おまけ
UserDefaults に set
できる値は Property List にあるオブジェクトだけなのでそれ以外はシリアライズして Data として保存する必要があります。
Property List 以外のオブジェクトを入れてると Exception が起きます。
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object XXXX.Team for key team'
NSCoding でシリアライズして Data 型で保存するできる メソッドを用意します。
archive
は key(文字列) を指定して、 NSCoding を継承したオブジェクトであれば UserDefaults に保存することができます。
extension UserDefaults { func archive<T: NSCoding>(key: String, value: T?) { if let value = value { self[key] = NSKeyedArchiver.archivedData(withRootObject: value) } else { self[key] = value } } func unarchive<T: NSCoding>(key: String) -> T? { return data(forKey: key) .map { NSKeyedUnarchiver.unarchiveObject(with: $0) } as? T } func unarchive<T: NSCoding>(key: String, defalutValue: T) -> T { let value = data(forKey: key) .map { NSKeyedUnarchiver.unarchiveObject(with: $0) } as? T return value ?? defalutValue } } /// 使い方 class Team: NSObject, NSCoding { var name = String() override init() {} required init?(coder aDecoder: NSCoder) { name = aDecoder.decodeObject(forKey: "name") as! String } func encode(with aCoder: NSCoder) { aCoder.encode(name, forKey: "name") } } let team = Team() team.name = "あんこう" let userDefaults = UserDefaults.standard userDefaults.archive(key: "team", value: team) let value: Team? = userDefaults.unarchive(key: "team") print(value?.name) // => Optional("あんこう")
というわけで UserDefaults の Tips でした。引き続き、Kotlin を勉強したいと思います。