ユニファ開発者ブログ

ユニファ株式会社システム開発部メンバーによるブログです。

Swift 4 で UserDefaults を簡単に扱う

はじめに

iOS エンジニアのしだです。年始から喉が痛くずっとガラガラ声だったり左目が腫れたり2018年はなんだか嫌な予感がします。

最近、Kotlin を勉強しています。もう ことりん という響きだけでいとおしいく感じてます。 Kotlin 勉強しているときに以下の記事を見まして便利だなぁと思ったので、iOS の UserDefaults を同じように扱いたいなと思って書いてみました。

medium.com

今回は iOS の UserDefaults の小ネタです。

準備

  • Xcode 9.1 (Swift 4)

UserDefaults

iOS の UserDefaults は、アプリ内で使える Key-Value ストアで、値は Property List として保存されます。 Android でいうところの SharedPreferences です。

UserDefaults のみ

普段 UserDefaults を使うときは、なにも考えずに以下のように書いてました。

struct Preference {
    static let userDefault = UserDefaults.standard

    struct Key {
        static let FirstLaunchedAt = "first_launched_at"
    }
}

extension Preference {

    static var firstLaunchedAt: Date? {
        get {
            return userDefault.object(forKey: Key.FirstLaunchedAt) as? Date
        }
        set {
            userDefault.set(newValue, forKey: Key.FirstLaunchedAt)
            userDefault.synchronize()
        }
    }
}


Preference.firstLaunchedAt = Date()
Preference.firstLaunchedAt! // => 2018-01-07 05:03:57 +0000

保存する値が 多くなると extension Preference に似たようなコードが増えて醜いなと感じていました。

UserDefaults + Generic Subscripts

Swift 4 から Generic Subscriptssubscript に Generic がサポートされたのでより簡潔に書けます。 ちょっと Android の SharedPreferences を意識して defaultValue も付けてみました。

extension UserDefaults {

    subscript<T: Any>(key: String, defaultValue: T) -> T {
        get {
            let value = object(forKey: key)
            return (value as? T) ?? defaultValue
        }
        set {
            set(newValue, forKey: key)
            synchronize()
        }
    }
    
    subscript<T: Any>(key: String) -> T? {
        get {
            let value = object(forKey: key)
            return value as? T
        }
        set {
            guard let newValue = newValue else {
                removeObject(forKey: key)
                return
            }
            set(newValue, forKey: key)
            synchronize()
        }
    }
}


let userDefaults = UserDefaults.standard 
userDefaults["first_launched_at"] = Date()
userDefaults["first_launched_at", Date()] // => 2018-01-07 05:20:37 +0000
let date: Date? = userDefaults["first_launched_at"] // => 2018-01-07 05:20:37 +0000

UserDefaults の Key を Enum で定義

enum 列挙型 といっしょに使えばもう少しわかりやすくなります。

enum Key: String {
    case firstLaunchedAt = "first_launched_at"
}

extension UserDefaults {
    
    subscript<T: Any>(key: Key, defaultValue: T) -> T {
        get { return self[key.rawValue, defaultValue] }
        set { self[key.rawValue] = newValue }
    }
    
    subscript<T: Any>(key: Key) -> T? {
        get { return self[key.rawValue] }
        set { self[key.rawValue] = newValue }
    }

    func remove(key: Key) {
        removeObject(forKey: key.rawValue)
    }
}


let userDefaults = UserDefaults.standard
userDefaults[.firstLaunchedAt] = Date()
userDefaults[.firstLaunchedAt, Date()] // => 2018-01-07 05:32:28 +0000
let date: Date? = userDefaults[.firstLaunchedAt] // => 2018-01-07 05:32:28 +0000

おまけ

UserDefaults に set できる値は Property List にあるオブジェクトだけなのでそれ以外はシリアライズして Data として保存する必要があります。

Property List 以外のオブジェクトを入れてると Exception が起きます。

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to insert non-property list object XXXX.Team for key team'

NSCoding でシリアライズして Data 型で保存するできる メソッドを用意します。 archive は key(文字列) を指定して、 NSCoding を継承したオブジェクトであれば UserDefaults に保存することができます。

extension UserDefaults {

    func archive<T: NSCoding>(key: String, value: T?) {
        if let value = value {
            self[key] = NSKeyedArchiver.archivedData(withRootObject: value)
        } else {
            self[key] = value
        }
    }
    
    func unarchive<T: NSCoding>(key: String) -> T? {
        return data(forKey: key)
            .map { NSKeyedUnarchiver.unarchiveObject(with: $0) } as? T
    }
    
    func unarchive<T: NSCoding>(key: String, defalutValue: T) -> T {
        let value = data(forKey: key)
            .map { NSKeyedUnarchiver.unarchiveObject(with: $0) } as? T
        return value ?? defalutValue
    }
}


/// 使い方

class Team: NSObject, NSCoding {
    var name = String()

    override init() {}

    required init?(coder aDecoder: NSCoder) {
        name = aDecoder.decodeObject(forKey: "name") as! String
    }
    
    func encode(with aCoder: NSCoder) {
        aCoder.encode(name, forKey: "name")
    }
}

let team = Team()
team.name = "あんこう"
let userDefaults = UserDefaults.standard
userDefaults.archive(key: "team", value: team)
let value: Team? = userDefaults.unarchive(key: "team")
print(value?.name) // => Optional("あんこう")

というわけで UserDefaults の Tips でした。引き続き、Kotlin を勉強したいと思います。