用Swift更優(yōu)雅地管理用戶偏好設(shè)置

閱讀原文

大部分項目都需要選擇一種數(shù)據(jù)持久化方案來保存用戶的偏好設(shè)置泽西,在iOS開發(fā)中一般選擇UserDefaults缰趋。下面先簡單介紹下一般的實現(xiàn)方式,然后再介紹下我在開源項目中學(xué)到的一種新姿勢味抖。

準(zhǔn)備

項目中除了要支持保存基本數(shù)據(jù)類型灰粮,也要支持自定義類型粘舟。為了簡化描述,本文只選擇了兩種不同的基本數(shù)據(jù)類型和一種自定義類型柑肴。其中launchAtLoginBool型晰骑,launchCountInt型,userInfo定義如下:

final class UserInfo: NSObject, NSCoding {
    var id = 0
    var name = ""
    
    convenience init(id: Int, name: String) {
        self.init()

        self.id = id
        self.name = name
    }
    
    convenience required init?(coder aDecoder: NSCoder) {
        self.init()

        for child in Mirror(reflecting: self).children {
            if let key = child.label {
                setValue(aDecoder.decodeObject(forKey: key), forKey: key)
            }
        }
    }
    
    func encode(with aCoder: NSCoder) {
        for child in Mirror(reflecting: self).children {
            if let key = child.label {
                aCoder.encode(value(forKey: key), forKey: key)
            }
        }
    }
}

注:由于自定義類需要遵循NScoding協(xié)議并編碼成Data格式才能通過UserDefaults保存隶症。以上代碼通過Mirror去遍歷對象屬性來實現(xiàn)NSCoding協(xié)議,可參考前一篇博文:利用Swift的反射機制簡化代碼淋样。當(dāng)然胁住,也可以將自定義類型轉(zhuǎn)換成字典彪见,然后通過UserDefaults提供的讀寫字典的方法訪問。

一般的方式

在較小的項目中由于偏好設(shè)置的數(shù)目和讀寫的次數(shù)不多余指,直接用下面這種一般的方式保存也不太會覺得哪里不妥或效率不高。

struct PreferenceKey {
    static let launchAtLogin = "LaunchAtLogin"
    static let launchCount = "LaunchCount"
    static let userInfo = "UserInfo"
}

func demo() {
    let userDefaults = UserDefaults.standard
    
    // Register default preferences.
    var userInfoData = NSKeyedArchiver.archivedData(withRootObject: UserInfo(id: 0, name: ""))
    let defaultPreferences: [String: Any] = [
        PreferenceKey.launchAtLogin: false,
        PreferenceKey.launchCount: 0,
        PreferenceKey.userInfo: userInfoData,
        ]
    userDefaults.register(defaults: defaultPreferences)
    
    // Test data.
    var launchAtLogin = true
    var launchCount = 10
    var userInfo = UserInfo(id: 121, name: "Fox")
    userInfoData = NSKeyedArchiver.archivedData(withRootObject: userInfo)
    
    // Write preferences.
    userDefaults.set(launchAtLogin, forKey: PreferenceKey.launchAtLogin)
    userDefaults.set(launchCount, forKey: PreferenceKey.launchCount)
    userDefaults.set(userInfoData, forKey: PreferenceKey.userInfo)
    
    // Read preferences.
    launchAtLogin = userDefaults.bool(forKey: PreferenceKey.launchAtLogin)
    launchCount = userDefaults.integer(forKey: PreferenceKey.launchCount)
    userInfoData = userDefaults.object(forKey: PreferenceKey.userInfo) as! Data
    userInfo = NSKeyedUnarchiver.unarchiveObject(with: userInfoData) as! UserInfo
    
    // Check preferences.
    for (key, value) in userDefaults.dictionaryRepresentation() {
        print("\(key): \(value)")
    }
}

更優(yōu)雅的方式

上面這種方式看著不那么優(yōu)雅碉碉,每次讀寫相應(yīng)設(shè)置也需要多敲一些代碼垢粮,降低了編碼效率靠粪,當(dāng)偏好設(shè)置選項多起來后缺點就更明顯了。另外昔善,讀取設(shè)置時需要知道該設(shè)置的類型捞慌,然后調(diào)用UserDefaults相應(yīng)的實例方法。如PreferenceKey.launchAtLoginBool型袖订,需要launchAtLogin = userDefaults.bool(forKey: PreferenceKey.launchAtLogin)嗅虏。讀取自定義類型時,還需要進(jìn)行類型轉(zhuǎn)換楞艾。如userInfo = userDefaults.object(forKey: PreferenceKey.userInfo) as! UserInfo

后來在開源編輯器項目CotEditor中看到了一種比較巧妙的實現(xiàn)方式蕴侧,最終可以通過類似Preferences[.launchAtLogin]的形式來讀寫相應(yīng)的偏好設(shè)置两入,看著有點像訪問字典的方式。實際上择葡,這種方式的主要工作就是實現(xiàn)類似字典的語法剃氧。下面就來看看如何實現(xiàn)這種訪問方式朋鞍。

Preference Key

在Swift中如果想讓自定義類作為Key去訪問某個集合中的元素,那么必須滿足兩個條件:

  • 用來訪問集合元素的Key類型本身需要遵循Hashable協(xié)議酝碳。
  • 集合實現(xiàn)了subscript操作符恨狈,支持通過方括號[]訪問集合元素呛讲。

Hashable

protocol Hashable : Equatable {
    var hashValue: Int { get }
}

Hashable協(xié)議中只定義了一個整型的哈希值hashValue,在Swift中任何遵循了該協(xié)議的類型都可以用來當(dāng)做SetDictionary的Key吗氏。官方文檔里面提到雷逆,在Swift標(biāo)準(zhǔn)庫中很多基本類型遵循了Hashable協(xié)議膀哲,如字符串、整型仿村、浮點型兴喂、布爾型等焚志。對于自定義類型也只要提供一個hashValue即可畏鼓。為了保證訪問字典時性能,需要保證同一個對象的哈希值相等岳悟,但不同對象的哈希值也是可以相等的泼差。其實我們可以利用String類型本身已經(jīng)實現(xiàn)了Hashable協(xié)議這點省去計算哈希值的工作堆缘,下面的RawRepresentable協(xié)議可以幫我們達(dá)到目的。

RawRepresentable

RawRepresentable協(xié)議一樣很簡潔录平,定義了一個初始化方法init?(rawValue:)缀皱,和rawValue

正如上面提到的表箭,Swift中的String類型本身就已經(jīng)遵循了Hashable協(xié)議钮莲,我們可以利用這一點直接在自定義類型中引入一個字符串類型的rawValue,并通過計算型屬性hashValue直接返回該字符串的hashValue极舔,即可達(dá)到遵循Hashable協(xié)議的目的链瓦。下面定義了PreferenceKey類型慈俯,最后它將作為key去訪問集合元素。

final class PreferenceKey<T>: PreferenceKeys { }

class PreferenceKeys: RawRepresentable, Hashable {
    let rawValue: String
    
    required init!(rawValue: String) {
        self.rawValue = rawValue
    }
    
    convenience init(_ key: String) {
        self.init(rawValue: key)
    }
    
    var hashValue: Int {
        return rawValue.hashValue
    }
}

extension PreferenceKeys {
    static let launchAtLogin = PreferenceKey<Bool>("LaunchAtLogin")
    static let launchCount = PreferenceKey<Int>("LaunchCount")
    static let userInfo = PreferenceKey<UserInfo>("UserInfo")
}

注:如果直接將靜態(tài)存儲屬性放在PreferenceKeys中溪掀,編譯時就會報錯:

Static stored properties not support in generic types.

CotEditor通過繼承PreferenceKeys步鉴,在子類PreferenceKey中支持泛型來巧妙地避開上面的問題。

Preference Manager

為了方便管理用戶的偏好設(shè)置喊递,可采用單例模式定義一個PreferenceManager骚勘,主要負(fù)責(zé)包括模塊初始化、注冊默認(rèn)配置当宴,提供訪問集合元素的方法等工作泽疆。

final class PreferenceManager {
    static let shared = PreferenceManager()
    let defaults = UserDefaults.standard
    
    private init() {
        registerDefaultPreferences()
    }
    
    private func registerDefaultPreferences() {
        // Convert dictionary of type [PreferenceKey: Any] to [String: Any].
        let defaultValues: [String: Any] = defaultPreferences.reduce([:]) {
            var dictionary = $0
            dictionary[$1.key.rawValue] = $1.value
            return dictionary
        }
        defaults.register(defaults: defaultValues)
    }
}

let defaultPreferences: [PreferenceKeys: Any] = [
    .launchAtLogin: false,
    .launchCount: 0,
    .userInfo: NSKeyedArchiver.archivedData(withRootObject: UserInfo(id: 0, name: "")),
]

上面的代碼中registerDefaultPreferences函數(shù)通過reduce方法將字典類型從[PreferenceKey: Any]轉(zhuǎn)換到[String: Any]殉疼,以便UserDefaults注冊默認(rèn)設(shè)置。

Subscript

為了支持通過方括號[]訪問PreferenceManager中的元素挂洛,還要實現(xiàn)subscript方法眠砾。這里雖然顯得有點繁瑣,項目中計劃支持的類型都需要寫一遍伙单,包括自定義類型哈肖,如UserInfo念秧。但也只是需要編寫一次即可摊趾,后面需要新增支持的類型時再添加新方法。

extension PreferenceManager {
    subscript(key: PreferenceKey<Any>) -> Any? {
        get { return defaults.object(forKey: key.rawValue) }
        set { defaults.set(newValue, forKey: key.rawValue) }
    }
    
    subscript(key: PreferenceKey<URL>) -> URL? {
        get { return defaults.url(forKey: key.rawValue) }
        set { defaults.set(newValue, forKey: key.rawValue) }
    }
    
    subscript(key: PreferenceKey<[Any]>) -> [Any]? {
        get { return defaults.array(forKey: key.rawValue) }
        set { defaults.set(newValue, forKey: key.rawValue) }
    }
    
    subscript(key: PreferenceKey<[String: Any]>) -> [String: Any]? {
        get { return defaults.dictionary(forKey: key.rawValue) }
        set { defaults.set(newValue, forKey: key.rawValue) }
    }
    
    subscript(key: PreferenceKey<String>) -> String? {
        get { return defaults.string(forKey: key.rawValue) }
        set { defaults.set(newValue, forKey: key.rawValue) }
    }
    
    subscript(key: PreferenceKey<[String]>) -> [String]? {
        get { return defaults.stringArray(forKey: key.rawValue) }
        set { defaults.set(newValue, forKey: key.rawValue) }
    }
    
    subscript(key: PreferenceKey<Data>) -> Data? {
        get { return defaults.data(forKey: key.rawValue) }
        set { defaults.set(newValue, forKey: key.rawValue) }
    }
    
    subscript(key: PreferenceKey<Bool>) -> Bool {
        get { return defaults.bool(forKey: key.rawValue) }
        set { defaults.set(newValue, forKey: key.rawValue) }
    }
    
    subscript(key: PreferenceKey<Int>) -> Int {
        get { return defaults.integer(forKey: key.rawValue) }
        set { defaults.set(newValue, forKey: key.rawValue) }
    }
    
    subscript(key: PreferenceKey<Float>) -> Float {
        get { return defaults.float(forKey: key.rawValue) }
        set { defaults.set(newValue, forKey: key.rawValue) }
    }
    
    subscript(key: PreferenceKey<Double>) -> Double {
        get { return defaults.double(forKey: key.rawValue) }
        set { defaults.set(newValue, forKey: key.rawValue) }
    }
    
    subscript(key: PreferenceKey<UserInfo>) -> UserInfo? {
        get {
            var object: UserInfo?
            if let data = defaults.data(forKey: key.rawValue) {
                object = NSKeyedUnarchiver.unarchiveObject(with: data) as? UserInfo
            }
            return object
        }
        set {
            if let object = newValue {
                let data = NSKeyedArchiver.archivedData(withRootObject: object)
                defaults.set(data, forKey: key.rawValue)
            }
        }
    }
}

到這里就差不多結(jié)束了。不過為了更方便地訪問止吐,再定義一個全局變量:

let Preferences = PreferenceManager.shared

下面是訪問偏好設(shè)置的最終方式:

func demo() {
    let userDefaults = UserDefaults.standard
    
    // Test data.
    var launchAtLogin = true
    var launchCount = 10
    var userInfo: UserInfo? = UserInfo(id: 123, name: "Fox")
    
    // Write preference.
    Preferences[.launchAtLogin] = launchAtLogin
    Preferences[.launchCount] = launchCount
    Preferences[.userInfo] = userInfo
    
    // Read preference.
    launchAtLogin = Preferences[.launchAtLogin]
    launchCount = Preferences[.launchCount]
    userInfo = Preferences[.userInfo]
    
    // Check preferences.
    for (key, value) in userDefaults.dictionaryRepresentation() {
        print("\(key): \(value)")
    }
}

相比開頭的那種方式是不是看起來很清爽碍扔?簡直和讀寫字典元素一毛一樣。我們通過這種方式把NSKeyedArchiver/NSKeyedUnarchiver的編解碼過程和UserDefaults的讀寫過程都封裝起來之后厉膀,即使是自定義類型的訪問也和基本類型毫無差別了二拐,也不用去操心我們訪問的設(shè)置是什么數(shù)據(jù)類型。

但不得不說這種方式目前還是有個小遺憾菱魔,那就是Xcode有時候會一臉懵逼變白板吟孙,但出現(xiàn)的次數(shù)也不多。這應(yīng)該是因為Swift類型推斷相對復(fù)雜藻治,導(dǎo)致Xcode有點吃不消巷挥。大不了通過添加類型,放棄這部分類型推斷倍宾,從而減小Xcode的負(fù)擔(dān)來解決,使用Preferences[PreferenceKey.launchAtLogin]來訪問钩乍。不過隨著Swift和Xcode的不斷優(yōu)化怔锌,相信這種情況會慢慢改善埃元。

后記

讀源碼是學(xué)習(xí)新招式的一個非常好的途徑。開始時可能只是學(xué)習(xí)和模仿岛杀,新招式積累多了类嗤,加上思考和總結(jié)辨宠,慢慢地自己也會有更多創(chuàng)造性的點子罪针。

如果大家有其他新姿勢,期待你們的分享派殷。我把代碼放到了GitHub上了墓阀,前一種方式對應(yīng)的Commit: 7d32ddb斯撮,后一種方式對應(yīng)的Commit: 0f26fb5,大家可以clone一份自己感受感受帕膜。

最后溢十,感謝CotEditor,感謝開源荒典。

參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末寺董,一起剝皮案震驚了整個濱河市刻剥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌盯滚,老刑警劉巖酗电,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件撵术,死亡現(xiàn)場離奇詭異话瞧,居然都是意外死亡寝姿,警方通過查閱死者的電腦和手機饵筑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進(jìn)店門处坪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來同窘,“玉大人,你說我怎么就攤上這事想邦∩ッ唬” “怎么了?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵漆际,是天一觀的道長拉庵。 經(jīng)常有香客問我钞支,道長,這世上最難降的妖魔是什么婴洼? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任撼嗓,我火速辦了婚禮且警,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘斑芜。我一直安慰自己,他們只是感情好盈包,可當(dāng)我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布呢燥。 她就那樣靜靜地躺著叛氨,像睡著了一般。 火紅的嫁衣襯著肌膚如雪力试。 梳的紋絲不亂的頭發(fā)上畸裳,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天,我揣著相機與錄音帅容,去河邊找鬼伍伤。 笑死扰魂,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的姐直。 我是一名探鬼主播蒋畜,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼插龄!你這毒婦竟也來了科展?” 一聲冷哼從身側(cè)響起才睹,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤见秤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后乎澄,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體置济,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年护盈,在試婚紗的時候發(fā)現(xiàn)自己被綠了腐宋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片檀轨。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡参萄,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出讹挎,到底是詐尸還是另有隱情筒溃,我是刑警寧澤,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布积蔚,位于F島的核電站烦周,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏漱贱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一夭委、第九天 我趴在偏房一處隱蔽的房頂上張望幅狮。 院中可真熱鬧,春花似錦、人聲如沸崇摄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽逐抑。三九已至鸠儿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間厕氨,已是汗流浹背进每。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工命斧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留田晚,地道東北人。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓国葬,卻偏偏與公主長得像贤徒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子胃惜,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內(nèi)容