今回はUserDefaultsの話になります。
UserDefaultsは手軽にデータの永続化ができるため、多くの場面で利用される機能です。
私はアプリ設定の保存によく使います。
このUserDefaultsについて、Swift3.0で大きな変更がありました。
まずは、Swift3.0の変更をおさらいします。
1. インスタンスの作成
NSUserDefaults.standardUserDefaults()
UserDefaults.standard
コードから見て分かるとおり、NSUserDefaultsからUserDefaultsに置き換わりました。
また、インスタンスの生成もstandardUserDefaults()メソッドからstandardプロパティに変更されています。
2. 保存
NSUserDefaults.standardUserDefaults().setBool(true, forKey: "boolKeyName")
NSUserDefaults.standardUserDefaults().setInt(1, forKey: "integerKeyName")
UserDefaults.standard.set(true, forKey: "boolKeyName")
UserDefaults.standard.set(1, forKey: "integerKeyName")
保存のためのメソッドは、**set(_:forKey:)**のみになりました。
コンパイラが第1パラメータの型を推測して、適切なメソッドが呼び出される仕組みになります。
3. 読み込み
NSUserDefaults.standardUserDefaults().boolForKey("boolKeyName")
NSUserDefaults.standardUserDefaults().integerForKey("integerKeyName")
UserDefaults.standard.bool(forKey: "boolKeyName")
UserDefaults.standard.integer(forKey: "integerKeyName")
メソッド名が少し短くなりました。
4. 削除
NSUserDefaults.standardUserDefaults().removeObjectForKey("keyName")
UserDefaults.standard.removeObject(forKey: "keyName")
5. 存在チェック
if NSUserDefaults.standardUserDefaults().objectForKey("KeyName") != nil { ... }
if UserDefaults.standard.object(forKey: "KeyName") != nil { ... }
6. Synchronize
非推奨になりました。
UserDefaultsの問題点
さて、記事のタイトルはUserDefaultsをSwiftらしく使うでしたが、まずはよく見かける方法でUserDefaultsを使ってみたいと思います。
struct Constants {
static let telKey = "tel"
}
// 電話番号
let tel = "000-0000-0000"
// 保存
UserDefaults.standard.set(tel, forKey: Constants.telKey)
// 読み込み
UserDefaults.standard.string(forKey: Constants.telKey)
UserDefaultsのキー名は、保存時・読み込む時等、複数回使用される可能性が高いため、定数にするのが一般的だと思います。
しかしこの方法だと文字列定数を使っているため、打ち間違えが発生しやすい問題があります。
私はtel(電話番号)を **"tell(伝える)"**と打ち間違えしてしまうことがあります。
打ち間違えをレビューで指摘されると、ちょっと辛いですよね、、、
打ち間違えによるミスを防ぐために、定数で定義している文字列をEnumに変更すると、少し良い感じになります。
struct Constants {
enum User : String {
case tel
}
}
// 電話番号
let tel = "000-0000-0000"
// 保存
UserDefaults.standard.set(tel, forKey: Constants.User.tel.rawValue)
// 読み込み
UserDefaults.standard.string(forKey: Constants.User.tel.rawValue)
Enumは列挙子にString型のRaw値「tel」を割り当てるとができるため、文字列で直接**"tel"**と記述する必要がなくなります。
SwiftらしいUserDefaults
Enumを使い、打ち間違えによるエラーを防ぐことができましたが、キー名に「Constants.User.tel.rawValue」と記述するのは、冗長的でSwiftらしくありません。
これをプロトコル拡張で、より短く簡潔にすることができます。
protocol KeyNamespaceable {
func namespaced<T: RawRepresentable>(_ key: T) -> String
}
extension KeyNamespaceable {
func namespaced<T: RawRepresentable>(_ key: T) -> String {
return "\(Self.self).\(key.rawValue)"
}
}
protocol StringDefaultSettable : KeyNamespaceable {
associatedtype StringKey : RawRepresentable
}
extension StringDefaultSettable where StringKey.RawValue == String {
func set(_ value: String, forKey key: StringKey) {
let key = namespaced(key)
UserDefaults.standard.set(value, forKey: key)
}
@discardableResult
func string(forKey key: StringKey) -> String? {
let key = namespaced(key)
return UserDefaults.standard.string(forKey: key)
}
}
extension UserDefaults : StringDefaultSettable {
enum StringKey : String {
case tel
}
}
// 電話番号
let tel = "000-0000-0000"
// 保存
UserDefaults.standard.set(tel, forKey: .tel) // キー名 UserDefaults.tel
// 読み込み
UserDefaults.standard.string(forKey: .tel) // キー名 UserDefaults.tel
UserDefaultsの保存、読み込みのコードがより短く簡潔になりました。
**"KeyNamespaceable"**は、UserDefaultsのキー名の衝突を防ぐためのプロトコルです。
KeyNamespaceableに適合させることで、キー名は「UserDefaults.tel」になりますが適合させないと、名前空間が含まれないただの「tel」になってしまうため、衝突が発生してしまう可能性が高いです。
**"StringDefaultSettable"**は、UserDefaultsへString型の値を「保存・読み込み」するためのプロトコルになります。
StringDefaultSettableの拡張メソッド set(_:forKey:)、**string(forKey:)**はUserDefaultsのAPIと同じになります。
このStringDefaultSettableをUserDefaultsに適合させることで、以下のようにSwiftらしいコードになります。
UserDefaults.standard.set(tel, forKey: .tel)
UserDefaults.standard.string(forKey: .tel)
また、Int型やDouble型等の値をUserDefaultsへ保存・読み込ませたい場合は、以下のようなプロトコルを作ると同様のことができます。
protocol IntegerDefaultSettable : KeyNamespaceable {...}
protocol DoubleDefaultSettable : KeyNamespaceable {...}
protocol FloatDefaultSettable : KeyNamespaceable {...}
protocol ObjectDefaultSettable : KeyNamespaceable {...}
protocol URLDefaultSettable : KeyNamespaceable {...}
protocol BoolDefaultSettable : KeyNamespaceable {...}
さらにSwiftらしく
実際にアプリ開発をする際、例えば**"tel(電話番号)"**のような情報は、モデルクラスのプロパティとして持つことが多いかと思います。
しかし、幸いにもStringDefaultSettableプロトコルは適合先をUserDefaultsに限定していないため、以下のようなモデルクラスにも適合させることができます。
struct User {
let name: String
let tel: String
}
extension User : StringDefaultSettable {
enum StringKey : String {
case name
case tel
}
}
UserDefaultsで**"tel"を持っていても何の電話番号かコードから読み取ることができませんが、Userで"tel"**を持てば、ユーザーの電話番号ということが明確になります。
おわりに
プロトコルを使ったSwiftらしいUserDefaultsは、打ち間違えやキー名の衝突を防止し、短く簡潔にコードを書くことができます。
そのため、Swiftではプロトコルを積極的に使っていくべきかと思います。