try! Swift では Gwendolyn WestonさんによるKeep Calm and Type Erase Onで紹介されていたType Erasure(型消去)がとても印象に残ったのですが、セッションを聞いただけでは使いどころがピンときませんでした。
(ポケモンを知らない自分のせいかもしれないですが😅)
型消去の復習を兼ねて、NSUserDefaultsを題材にして TypeDefaults というライブラリを作ってみたので、その紹介です。
TypedDefaults is a utility library to type-safely use NSUserDtefaults.
TypeDefaultsの特徴
-
NSUserDefaultsでカスタム型をType Safeに扱える -
Dependency Injection対応
NSUserDefaultsでカスタム型をType Safeに扱える
カスタム型をType SafeにNSUserDefaultsに保存できます。
カスタム型は後述するDefaultConvertibleプロコトルに準拠していればよく、NSObjectを継承する必要はありません。
そのため、カスタム型には SwiftのネイティブClass, Struct, Enum が利用可能です。(もちろん、NSObjectのサブクラスも可)
DefaultConvertibleプロトコル
public protocol DefaultConvertible {
static var key: String { get }
init?(_ object: AnyObject)
func serialize() -> AnyObject
}
カスタム型は、NSUserDefaultsにAnyObjectで保存します。
- 保存時:
serialize() - 取得時に
init?(_ object:)
が呼び出されます。
また、カスタム型はアプリで利用する個々の設定に対応することを前提にしています。
カスタム型とキーを1:1に対応付けするため、型プロパティとしてkeyを用意しています。
カスタム型の例
ここでは、カメラ設定として カメラロールへの保存フラグ 保存サイズを持つ型を例にしています。
/// カメラ設定として `カメラロールへの保存フラグ` `保存サイズ`を持つ型
struct CameraConfig: DefaultConvertible {
enum Size: Int {
case Large, Medium, Small
}
var saveToCameraRoll: Bool
var size: Size
// MARK: DefaultConvertible
static let key = "CameraConfig"
init?(_ object: AnyObject) {
guard let dict = object as? [String: AnyObject] else { return nil }
self.saveToCameraRoll = dict["cameraRoll"] as? Bool ?? true
if let rawSize = dict["size"] as? Int,
let size = Size(rawValue: rawSize) {
self.size = size
} else {
self.size = .Medium
}
}
func serialize() -> AnyObject {
return ["cameraRoll": saveToCameraRoll, "size": size.rawValue]
}
}
利用方法
カスタム型をNSUserDefaultsへ保存するためには、NSUserDefaultsへのアクセスを中継する型であるPersistentStoreを利用します。
利用方法は次の通りです。
/// 利用するカスタム型をインスタンス生成時に指定します。
let userDefaults = PersistentStore<CameraConfig>()
// CameraConfigをインスタンス化
var cs = CameraConfig([:])!
// set
userDefaults.set(cs)
// get
userDefaults.get()?.size // Medium
/// カスタム型のサイズを変更
cs.size = .Large
// set
userDefaults.set(cs)
// get
userDefaults.get()?.size // Large
Dependency Injection 対応
NSUserDefaultsはデータを永続化するため、Unit Testと相性がよくありません。
NSuserDefaultsに保存したカスタム型の値によって挙動が変わる型をテストするために、DIに使える型 InMemoryStore AnyStore を用意しています。
InMemoryStoreは PersistentStoreと同じDefaultStoreTypeに準拠しており、インターフェースは基本同じです。
InMemoryStoreはセットされた値をメモリ上で保持し永続化しない点がPersistentStoreと異なります。
一方、AnyStoreは InMemoryStoreとPersistentStore かを抽象化する型です。
ここでType Erasureを使ってます。
サンプル
テスト時にInMemoryStore AnyStore を使う例です。
次のようなCameraViewControllerというUIViewControllerを継承したクラスがあったとします。
NSUserDefaultsに保存したカスタム型を保持するプロパティconfigをDI可能にするためAnyStore型にします。
/// アプリのコード
class CameraViewController: UIViewController {
lazy var config: AnyStore<CameraConfig> = {
let ds = PersistentStore<CameraConfig>()
return AnyStore(ds)
}()
...
}
プロパティconfigの型をPersistentStoreではなくAnyStoreにしたことで、テスト時は次のようにInMemoryStoreに差し替えることが可能となります。
/// テストコード
class CameraViewControllerTests: XCTestCase {
var viewController: CameraViewController!
override func setUp() {
viewController = CameraViewController()
let defaultConfig = CameraConfig([:])!
let ds = InMemoryStore<CameraConfig>()
ds.set(defaultConfig) //
viewController.config = AnyStore(ds)
}
}
まとめ
NSUserDefaultsをユースケースとして、Type Erasureを復習してみました。
Type Erasure自体は抽象的な手法だと思うのですが、意外にアプリ開発の現場で結構使いどころがありそうですね。
冒頭でも書きましたが、この記事で使ったコードはライブラリ化してGithubに公開しています。
もしよかったら、使ってみて下さい。
改善の余地は色々あると思うので、PRやIssueでフィードバック頂けると幸いです。
TypedDefaults is a utility library to type-safely use NSUserDefaults.