will and way

ただの自分用メモを人に伝える形式で書くことでわかりやすくまとめてるはずのブログ

UserDefaultsをSwiftのEnumで扱えるように拡張する

UserDefaultsって便利ですよね。

基本的にはユーザーのアプリ内の設定値保存に使うことを主としていますが、
キルされても保持したいけど、アンインストール→インストールでは消されてもいい。DBを作るまでもないといった
値の軽いキャッシュとして利用したりもできます。

UserDefaultsのキーはString...

Stringって安易に使うとバギーですよね。

どこかでKeyを集約書けばよくね?って思いますがそれってつまり列挙なんです。 ということで、enumでラップして使ってみました。

アプローチ

  1. キーのprotocol UserDefaultKeyを定義
  2. UserDefaultKeyを引数にとって各々の型の値を返すextensionを定義
  3. UserDefaultKeyを使ったUserDefaultのデータストアの実装

1. キーのprotocol UserDefaultKeyを定義

public protocol UserDefaultKey: RawRepresentable, CustomStringConvertible { }

public extension UserDefaultKey where RawValue == String {
    public var description: String {
        return self.rawValue
    }
}

Enumって実はよしなにコンパイラが作ってくれるEquatableRawRepresentableに準拠しているstructのstaticな変数の列挙のようなものです。
そこでEnumを包含する方の定義としてRawRepresentableを定義しておきます。

今回はCustomStringConvertible.descriptionをキーとして使う戦略です。

なので、RawValueがStringのときはdescriptionの値としてself.rawValueを返せばいいことになります

したがって下記のようにenumを作るだけで、キーとして扱えるということです

enum DefaultKeys: String, UserDefaultKey {
    case autoReloadEnabled
}

2. UserDefaultKeyを引数にとって各々の型の値を返すextensionを定義

UserDefaultKeyを受け取れるインターフェースとして定義しています。
これで透過的にUserDefaultKeyをキーとして扱えるようになりました

// MARK: getter
public extension UserDefaults {
    public func object<Key: UserDefaultKey>(forKey key: Key) -> Any? {
        return object(forKey: key.description)
    }
    public func url<Key: UserDefaultKey>(forKey key: Key) -> URL? {
        return url(forKey: key.description)
    }
    
    public func array<Key: UserDefaultKey>(forKey key: Key) -> [Any]? {
        return array(forKey: key.description)
    }
    
    public func dictionary<Key: UserDefaultKey>(forKey key: Key) -> [String: Any]? {
        return dictionary(forKey: key.description)
    }
    
    public func string<Key: UserDefaultKey>(forKey key: Key) -> String? {
        return string(forKey: key.description)
    }
    
    public func stringArray<Key: UserDefaultKey>(forKey key: Key) -> [String]? {
        return stringArray(forKey: key.description)
    }
    
    public func data<Key: UserDefaultKey>(forKey key: Key) -> Data? {
        return data(forKey: key.description)
    }
    
    public func bool<Key: UserDefaultKey>(forKey key: Key) -> Bool? {
        return bool(forKey: key.description)
    }
    
    public func integer<Key: UserDefaultKey>(forKey key: Key) -> Int? {
        return integer(forKey: key.description)
    }
    
    public func float<Key: UserDefaultKey>(forKey key: Key) -> Float? {
        return float(forKey: key.description)
    }
    
    public func double<Key: UserDefaultKey>(forKey key: Key) -> Double? {
        return double(forKey: key.description)
    }
}

// MARK: - setter
public extension UserDefaults {
    public func set<Key: UserDefaultKey>(_ value: Any?, forKey key: Key) {
        set(value, forKey: key.description)
    }
    
    public func set<Key: UserDefaultKey>(_ value: URL?, forKey key: Key) {
        set(value, forKey: key.description)
    }
    
    public func set<Key: UserDefaultKey>(_ value: Bool, forKey key: Key) {
        set(value, forKey: key.description)
    }
    
    public func set<Key: UserDefaultKey>(_ value: Int, forKey key: Key) {
        set(value, forKey: key.description)
    }
    
    public func set<Key: UserDefaultKey>(_ value: Float, forKey key: Key) {
        set(value, forKey: key.description)
    }
    
    public func set<Key: UserDefaultKey>(_ value: Double, forKey key: Key) {
        set(value, forKey: key.description)
    }
}

3. UserDefaultKeyを使ったUserDefaultのデータストアの実装

protocol UserDefaultsDataStore {
    var autoReloadEnabled: String? { get set }
}

fileprivate enum UserDefaultsDataStoreKeys: String, UserDefaultKey {
    case autoReloadEnabled
}

struct UserDefaultsDataStoreImpl: UserDefaultsDataStore {
    var autoReloadEnabled: Bool {
        get {
            return bool(forKey: .autoReloadEnabled) ?? false
        }
        set {
            set(value: newValue, forKey: .authToken)
        }
    }

    private var defaults: UserDefaults {
        return UserDefaults.standard
    }
}

private extension UserDefaultsDataStoreImpl {
    func object(forKey key: UserDefaultsDataStoreKeys) -> Any? {
        return defaults.object(forKey: key)
    }
    
    func url(forKey key: UserDefaultsDataStoreKeys) ->URL? {
        return defaults.url(forKey: key)
    }
    
    func array(forKey key: UserDefaultsDataStoreKeys) ->[Any]? {
        return defaults.array(forKey: key)
    }
    
    func dictionary(forKey key: UserDefaultsDataStoreKeys) ->[String: Any]? {
        return defaults.dictionary(forKey: key)
    }
    
    func string(forKey key: UserDefaultsDataStoreKeys) ->String? {
        return defaults.string(forKey: key)
    }
    
    func stringArray(forKey key: UserDefaultsDataStoreKeys) ->[String]? {
        return defaults.stringArray(forKey: key)
    }
    
    func data(forKey key: UserDefaultsDataStoreKeys) ->Data? {
        return defaults.data(forKey: key)
    }
    
    func bool(forKey key: UserDefaultsDataStoreKeys) ->Bool? {
        return defaults.bool(forKey: key)
    }
    
    func integer(forKey key: UserDefaultsDataStoreKeys) ->Int? {
        return defaults.integer(forKey: key)
    }
    
    func float(forKey key: UserDefaultsDataStoreKeys) ->Float? {
        return defaults.float(forKey: key)
    }
    
    func double(forKey key: UserDefaultsDataStoreKeys) ->Double? {
        return defaults.double(forKey: key)
    }

    func set<V>(value: V?, forKey key: UserDefaultsDataStoreKeys) {
        if let v = value {
            defaults.set(v, forKey: key)    
            defaults.synchronize()
        }
    }
}

DataStore側でもgetterを定義していて若干冗長ですが、.autoReloadEnabledのようにEnumを省略形で書くためそうしています。

今回はUserDefaultKeyというprotocolを用意しましたが
もし、enumは1個しか定義しないと仮定するならば
具象enumを引数に取るUserDefaultsのextensionを書けばいいのでもっと簡潔にはできると思います。