Edited at

【iOS】UserDefaultsをSwiftらしく使う

More than 1 year has passed since last update.

今回は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ではプロトコルを積極的に使っていくべきかと思います。


参考

Swift Eye for the Stringly Typed API