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.