UserDefaults を型安全に使用する
キーのタイプミスと型の取り違えを防ぐために、UserDefaults をラップする方法と Mirror を使う方法の 2 つを紹介しています。
- 公開日
この記事は別サイトにも掲載しています。 https://qiita.com/iganin/items/166bf2a1a4b7f94c5e01
はじめに
UserDefaults を使用したアプリ内のデータ保存はアプリ内データの永続化のためによく取られる手法の一つです。SQLite3 などの DB 保存などと異なり、SQL 文を記載する必要もなく、非常に簡単にデータの保存を行うことができます。ただし、以下のように型に関する安全性がないこと、タイプミスによってデータ保存・取得に失敗することが問題となりえます。
let value = "hoge"
UserDefaults.standard.set(value, forKey: "someValue")
// some と same でタイプミスをしているので意図した値をえられない
// タイプミスをしていなかったとしても "someValue" に紐づく値は String のため int で取り出せない
let retrievedValue = UserDefaults.standard.int(forKey: "sameValue")
本記事では、UserDefaults の値の保存・取得で一般的と考えられる UserDefaults をラップする方法と Jean-David Gadina 氏によって提案されていた Mirror を使用した方法をご紹介します。
検証環境
以下の環境を使用しています。
- macOS Sierra Version 10.12.6
- Xcode Version 9.0.0
- iOS 11.0
UserDefaults をラップする方法
まず、UserDefaults をラップする方法です。 下記のように UserDefaults の get と set のメソッドをラップすることで、Key をアプリ内その都度指定する必要がなく、タイプミスをする可能性を減らすことができる、UserDefaults の value に型を保証することができます。ただ、1、2 個のプロパティに対してこの方法を行うのは良いのですが、クラス内の複数のプロパティに対して、実施する場合少々記述が冗長になると考えられます。
public var someSettingValue: Int {
get {
return UserDefaults.standard.integer(forKey: "someSettingValue")
}
set(value) {
UserDefaults.standard.set(value, forKey: "someSettingValue")
}
}
Mirror を使用した方法
以下で、Mirror を使用した方法をご紹介します。まずは、Mirror についての概要を記載し、その後に Mirror を使用した UserDefaults を型安全にし、タイプミスによる影響が極力減るようにする方法をご紹介します。
Mirror について
Swift にて Reflection を実現する構造体です。Mirror(reflecting: object) とすることで、object に代入したオブジェクト、構造体の型や、プロパティの型、値およびメソッドの情報を取得・操作することができます。
Mirror を使用して取得できる値の例は以下です。
- displayStyle - object が class か enum かといった情報を提供します
- subjectType - object の型がわかります
- superclassMirror - object の superClass の型がわかります
- children - object のプロパティの一覧が取得できます
public class Sample: NSObject {
public var sampleInt = 32
public var sampleString = "sample"
}
let mirror = Mirror(reflecting: Sample())
print(mirror.displayStyle)
print(mirror.subjectType)
print(mirror.superclassMirror)
mirror.children.map{ print($0) }
// 以下、出力
Optional(class)
Sample
Optional(Mirror for NSObject)
(label: Optional("sampleInt"), value: 32)
(label: Optional("sampleString"), value: "sample")
記載にあたっては以下の記事が大変参考になりました。
Mirror を使用して UserDefaults を型安全にする
本題です。上記でご紹介した Mirror を使用して、UserDefaults を使用した値の保存・取得を型安全にします。大まかな流れとしては下記となります。
- 作成したクラスのプロパティを KVO として登録する
- observeValue で値の変化を反映させる
※ 上記で Key の取得に Mirror.children を使用する
以下コードを貼ります。(元のコードを一部変更しています)
import UIKit
public class Preferences: NSObject {
// KVO で使用するために @objc と dynamic を宣言する
// ※ dynamic を宣言することで Objective-C のランタイムで使用できる
@obj dynamic var someIntegervalue: Int = 0
@obj dynamic var someStringValue: NSString = ""
@obj dynamic var someOptionalArrayValue: Array?
// シングルトンとする
static let shared = Preferences()
private override init() {
super.init()
// Mirror の children を使って、プロパティ名を取得
// 初期化のタイミングで UserDefaults から値を取得し、
// nil でない(値が存在する)場合は本クラスのプロパティにセット
// UserDefaults からの値の取得はこのタイミングのみ
for child in Mirror(reflecting: self).children {
guard let key = child.label else { continue }
if let value = UserDefaults.standard.object(forKey: key) {
self.setValue(value, forKey: key)
}
// 値の変化を検知できるようにプロパティを登録
addObserver(self, forKeyPath: key, options: .new, context: nil)
}
}
deinit {
// init で add した要素の observe を外す
for child in Mirror(reflecting: self).children {
guard let key = child.label else { continue }
self.removeObserver(self, forKeyPath: key)
}
}
// observeValue で値の変更を UserDefaults に反映させる
// ここで、key の取得に Mirror を使用する
public override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
for child in Mirror(reflecting: self).children {
guard let key = child.label else { continue }
if (key == keyPath) {
UserDefaults.standard.set(change?[.newKey], forKey: key)
UserDefaults.standard.synchronize()
break
}
}
}
}
UserDefaults を保存・取得するプロパティをまとめたクラスを作成します。KVO として登録したり、observeValue で値の変化を反映させる際の Key の取得に Mirror を使用します。枠組みさえ作ってしまえば、本クラスにプロパティを定義するのみで UserDefaults に保存・取得されるようになります。UserDefaults に保存する値の型が保証され、タイプミスの影響は無くなります。(Mirror を使用して Key を取得するため、文字列を自身で打つタイミングがないためです)
所感
UserDefaults に保存するプロパティ量が多く、ラップすることによるコードの煩雑さが増す場合は選択肢の一つとして考えても良いかもしれません。ただ、原文にもありますが、もともと Objective-C で採用した方法を Swift に移したようですので、Swift に適したより良い方法はあるかもしれません。