はじめに
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
- iOS11.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")
記載にあたっては以下の記事が大変参考になりました。
Swift reflectionについて
Javaにおけるリフレクションについて
The Swift Reflection API and what you can do with it
Mirrorを使用してUserDefaultsを型安全にする
本題です。上記でご紹介したMirrorを使用して、UserDefaultsを使用した値の保存・取得を型安全にします。大まかな流れとしては下記となります。
- 作成したクラスのプロパティをKVOとして登録する
- observeValueで値の変化を反映させる
- ※上記でKeyの取得にMirror.childrenを使用する
以下コードを貼ります。(元のコードを一部変更しています)
import UIKit
public class Preferences: NSObject {
// KVOで使用するために@objcとdynamicsを宣言する
// ※dynamicsを宣言することで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のchildlenを使って、プロパティ名を取得
// 初期化のタイミングで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で値の変更をUserDefalutsに反映させる
// ここで、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に適したより良い方法はあるかもしれません。