LoginSignup
21

More than 5 years have passed since last update.

try! Swiftの復習: 型消去を利用したライブラリを作ってみた

Last updated at Posted at 2016-03-26

try! Swift では Gwendolyn WestonさんによるKeep Calm and Type Erase Onで紹介されていたType Erasure(型消去)がとても印象に残ったのですが、セッションを聞いただけでは使いどころがピンときませんでした。
(ポケモンを知らない自分のせいかもしれないですが😅)

型消去の復習を兼ねて、NSUserDefaultsを題材にして TypeDefaults というライブラリを作ってみたので、その紹介です。

tasanobu/TypedDefaults

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
}

カスタム型は、NSUserDefaultsAnyObjectで保存します。

  • 保存時: 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 を用意しています。

InMemoryStorePersistentStoreと同じDefaultStoreTypeに準拠しており、インターフェースは基本同じです。
InMemoryStoreはセットされた値をメモリ上で保持し永続化しない点がPersistentStoreと異なります。

一方、AnyStoreInMemoryStorePersistentStore かを抽象化する型です。
ここで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でフィードバック頂けると幸いです。

tasanobu/TypedDefaults

TypedDefaults is a utility library to type-safely use NSUserDefaults.

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
21