259
261

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

【iOS】UserDefaultsをSwiftらしく使う

Last updated at Posted at 2016-12-05

今回は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

259
261
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
259
261

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?