LoginSignup
2
1

More than 3 years have passed since last update.

UserDefaultsをGenerics型とenumとprotocolを使って責務管理する

Last updated at Posted at 2019-06-03

はじめに

UserDefaultsはKVSで簡単にデータを永続化することができるだけでなく、Sharedインスタンスのため場所を選ばず操作することができて便利ですよね。
しかし、安易に多用してしまうと何処でどのように使われているか把握するだけでも一苦労です。合わせて、UserDefaultsのキーの管理も衝突が起きないようなルールを決めて運用する必要があります。

私自身何が一番良い方法かまだまだ模索中ですが、Generics型とenumとprotocolを組み合わせることで参照時のTypoの防止や用途に応じたアクセス管理が実現できたのでご紹介いたします。
もっと良い方法等ございましたら、ぜひご指摘ください!

よくある使い方

このような使い方が一般的な読み書きだと思います。

UserDefaults.standard.set("abc", forKey: "settingValue")
let value = UserDefaults.standard.string(forKey: "settingValue")

抱えていた課題

使っていて感じていた、課題は下記の通りでした。
1. 使用しているキーをが重複管理できない
2. Typoが発生しやすい
3. 意図しないクラスでの参照、書き込みができてしまう

「1.」や「2.」はenum化することで、ある程度は解消できますが「3.」が解消できず、長年もやもやしていました。

課題解決後

実行部分

let animalUserDefault = GenericsUserDefault<UserDefaultsKeys.Animal>()
animalUserDefault.set("tama", forKey: .cat)

let fruitUserDefault = GenericsUserDefault<UserDefaultsKeys.Fruit>()
fruitUserDefault.set("mikan", forKey: .orange)

上記は同じGenericsUserDefaultクラスのインスタンスですが、型を指定することで、別のUserDefaultとして振る舞うことができます。
ソースコードだけ見ても、利点が感じられないと思いますので、コード補完中のスクリーンショットを載せます。
Animal型のUserDefaultsの場合にはキーがAnimalのものだけ表示されています。
スクリーンショット 2019-06-04 2.49.34.png

一方、Fruit型のUserDefaultsの場合にはキーがFruitのものだけ表示されています。
スクリーンショット 2019-06-04 2.49.47.png
このように、必要な分だけキーが表示されるため、不用意に別のキーを参照することを防ぐことができます。

実装

キー宣言部分

UserDefaults.swift

struct UserDefaultsKeys {
    enum Animal: Identifier, UserDefaultsKeyProtocol {
        static let domain: Domain = .animal
        var id: Identifier { return self.rawValue }

        case cat
        case dog
        case rabbit
    }

    enum Fruit: Identifier, UserDefaultsKeyProtocol {
        static let domain: Domain = .fruit
        var id: Identifier { return self.rawValue }

        case apple
        case orange
        case grape
    }
}

Genericsクラス

typealias Identifier = String

// UserDefaultsを分割したい単位をドメインとして管理する
enum Domain: String {
    case animal // 動物関連のドメイン
    case fruit // 果物関連のドメイン
}

// 抽象化されたUserDefaultsのキー
protocol UserDefaultsKeyProtocol {
    static var domain: Domain { get }
    var id: Identifier { get }
    var key: String { get }
}

// キーは共通でドメインとドメイン配下のIDの組み合わせで一意化します。
extension UserDefaultsKeyProtocol {
    var key: String { return Self.domain.rawValue + "." + id }
}

// Generics型Tを加えることで、初期化時にどのドメインにアクセスするか固定することができます。
final class GenericsUserDefault<T: UserDefaultsKeyProtocol> {
    // ベースとなるUserDefaults
    private let userDefaults: UserDefaults

    init(_ userDefaults: UserDefaults = .standard) {
        self.userDefaults = userDefaults
    }

    // Generics型Tがあるため、指定のドメイン配下のキーのみを指定できるようになります。
    func set(_ value: Any?, forKey identifier: T) {
        userDefaults.set(value, forKey: identifier.key)
    }
    // 同様にGenerics型Tにすることで取り出せる値も制限することができます。
    func string(forKey identifier: T) -> String? {
        return userDefaults.string(forKey: identifier.key)
    }
}

さらなる課題

DI等のモック化・テスト性が次の課題と考えています。

最後に

いかがでしたでしょうか、UserDefaultsは使いやすく自由度が高いため強力なツールです。強力すぎるが故に、管理ができなくなってしまうということにならないように適度に制限をかけて行くのが良いかなと個人的には思います。

今回のコードはこちらに載せておきます。
ご不明な点などあればコメントください。

2
1
0

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
2
1