Swift:UserDefaults 协议( Swift 视角下的泛字符串类型 API )

作者:Andyy Hope,原文链接,原文日期:2016-11-01
译者:Yake;校对:pmst;定稿:CMB

无论是从语言本身还是项目代码,Swift3 的革新无疑是一场“惊天海啸” ,一些读者可能正奋战在代码迁移的前线。但即使有如此之多的改动, Swift 中依旧存在许多基于 Foundation 框架,泛字符串类型的 API 。这些 API 完全没有问题,只是...

我们对这种 API 有一种既爱又恨的感情:偏爱它的灵活性;又恨一时粗心导致问题接踵而来。这简直是在刀尖上编程。

Foundation 框架的开发者们之所以提供泛字符串类型的接口,是考虑到无法准确预见我们未来会如何使用这个框架。这些开发者们极尽自己的智慧、能力和知识,最终决定在某些 API 中使用字符串,这为我们开发人员带来了无尽的可能性,也可以说是一种黑魔法。

UserDefaults

今天的主题是我学习 iOS 开发初期最先熟悉的 API 之一。对于那些不熟悉它的人来说,它不过是对一系列信息的持久化存储,例如一张图片,一些应用的设置等。部分开发者偏向于认为它是"轻量级的 Core Data 。尽管人们绞尽脑汁想要把它作为替代品楔入,但结果表明它还远远不够强大。

Stringly typed API

UserDefaults.standard.set(true, forKey: “isUserLoggedIn”)
UserDefaults.standard.bool(forKey: "isUserLoggedIn")

这是 UserDefaults 在平常应用中的基础用法,它向我们提供了持久存储和取值的简单方法,在应用中随处可以覆盖或者删除数据。由于缺少一致性和上下文,我们一不小心就会犯错,但更有可能是拼写错误。在这篇文章当中,我们将会改变 UserDefaults 在通常意义上的特性,并根据我们的需要进行定制。

使用常量

let key = "isUserLoggedIn"
UserDefaults.standard.set(true, forKey: key)
UserDefaults.standard.bool(forKey: key)

如果你遵从这种奇妙的技巧,我保证你很快就能将代码写得更好。如果你需要多次重复使用一个字符串,那么将它转换成一个常量,并在你的余生一直遵守这种规则,然后记得下辈子谢谢我。

分组常量

struct Constants {
    let isUserLoggedIn = "isUserLoggedIn"
}
...
UserDefaults.standard
   .set(true, forKey: Constants().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants().isUserLoggedIn)

一种可以帮我们维持一致性的模式就是将我们所有重要的默认常量分组写在同一个地方。这里我们创建了一个常量结构体来存储并指向我们的默认值。

还有一个建议是将你的属性名字设置成它对应的值,尤其是跟默认值打交道的时候。这样做可以简化你的代码并使属性在整体上有更好的一致性。拷贝属性名,将他们粘贴在字符串中,这样可以避免拼写错误。

let isUserLoggedIn = "isUserLoggedIn"

添加上下文

struct Constants {
    struct Account
        let isUserLoggedIn = "isUserLoggedIn"
    }
}
...
UserDefaults.standard
   .set(true, forKey: Constants.Account().isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account().isUserLoggedIn)

创建一个常量结构体完全没有问题,但是在我们写代码的时候记得提供上下文。我们努力的目标是让自己的代码对任何人都具有较高的可读性,包括我们自己。

Constants().token // Huh?

token 是什么意思?当有人试图搞清楚这个 token 的意义是什么的时候,缺少命名空间上下文使得新人或者不熟悉代码的人很难搞清楚这意味着什么,甚至包括一年后的原作者。

Constants.Authentication().token // better

避免初始化

struct Constants {
    struct Account
        let isUserLoggedIn = "isUserLoggedIn"
    }
    private init() { }
}

我们绝对不打算,也不想让常量结构体被初始化,所以我们把初始化方法声明为私有方法。这只是一个预防性措施,但我仍然推荐这么做。至少这样做可以避免我们在只想要静态变量时却不小心声明了实例变量。说到静态变量...

静态变量

struct Constants {
    struct Account
        static let isUserLoggedIn = "isUserLoggedIn"
    }
    ...
}
...
UserDefaults.standard
   .set(true, forKey: Constants.Account.isUserLoggedIn)
UserDefaults.standard
   .bool(forKey: Constants.Account.isUserLoggedIn)

你可能已经注意到了,我们每次获取 key ,都需要初始化它所属的结构体。与其每次都这么做,我们不如把它声明为静态变量。

我们使用 static 而非 class 关键字,是因为结构体作为存储类型时只允许使用前者。依据 Swift 的编译规则,结构体不能使用 class 声明属性。但如果你在一个类中使用 static 声明属性,这跟使用 final class 声明属性是一样的。

final class name: String
static name: String
// final class == static

使用枚举类型避免拼写错误

enum Constants {
    enum Account : String {
        case isUserLoggedIn
    }
    ...
}
...
UserDefaults.standard
    .set(true, forKey: Constants.Keys.isUserLoggedIn.rawValue)
UserDefaults.standard
    .bool(forKey: Constants.Keys.isUserLoggedIn.rawValue)

文章中我们提到了,为了一致性我们需要使属性能反映出他们的值。这里我们会将这种一致性更进一步,采用 enum case 来代替 static let 来将这个过程自动化。

你可能已经注意到了,我们已经创建了 Account 并让其遵守 String 协议,而 Stirng 遵守了 RawRepresentable 协议。这么做是因为,如果我们不给每个 case 提供一个 RawValue ,这个值将和声明的 case 保持一致。这么做会减少很多手动的输入或者复制粘贴字符串,减少错误的发生。

// Constants.Account.isUserLoggedIn.rawValue == "isUserLoggedIn"

到目前为止我们已经使用 UserDefaults 做了一些很酷的事情,但其实我们做的还不够。最大的问题是我们仍然在使用泛字符串类型 API ,即使我们已经对字符串做了一些修饰,但对于项目来说还不够好。

在我们的认知中,语言提供给我们什么,我们就只能干什么。然而 Swift 是一门如此棒的语言,我们已经在挑战过去写 Objective-C 时学习到和了解的知识。接下来,让我们回到厨房给这些 API 加些语法糖作料。

API 目标

UserDefaults.standard.set(true, forKey: .isUserLoggedIn) 
// #APIGoals

下面,我们会力争创建一些在与 UserDefaults 打交道时更好用的 API ,以此满足我们的需要。而比较好的做法莫过于使用协议扩展。

BoolUserDefaultable

protocol BoolUserDefaultable {
    associatedType BoolDefaultKey : RawRepresentable
}

首先我们来为布尔类型的 UserDefalts 创建一个协议,这个协议很简单,没有任何变量和需要实现的方法。然而,我们提供了一个叫做 BoolDefaultKey 的关联类型,这个类型遵守 RawRepresentable 协议,接下来你会明白为什么这么做。

扩展

extension BoolUserDefaultable 
    where BoolDefaultKey.RawValue == String { ... }

如果我们准备遵守协议的 Crusty 定律,首先声明一个协议扩展。并且使用一个 where 句法,限制扩展只适用于关联类型的 RawValue 是字符串的情况。

每一个协议,都有一个相当且相符合的协议扩展- Crusty 第三定律。

UserDefaultSetter 方法

// BoolUserDefaultable extension
static func set(_ value: Bool, forKey key: BoolDefaultKey) {
    let key = key.rawValue
    UserDefaults.standard.set(value, forKey: key)
}
static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = key.rawValue
    return UserDefaults.standard.bool(forKey: key)
}

是的,这是对标准 UserDefaultsAPI 的简单封装。我们这么做是因为这样代码的可读性会更高,因为你只需传入简单的枚举值而不需要传入冗长的字符串(校对者注:摒弃类似下面 Aint.Nobody.Got.Time.For.this.rawValue 这种路径式字符串)。

UserDefaults.set(false, 
    forKey: Aint.Nobody.Got.Time.For.this.rawValue)

一致性

extension UserDefaults : BoolUserDefaultSettable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn
    }
}

是的,我们准备扩展 UserDefaults ,让它遵守 BoolDefaultSettable 并提供一个名叫 BoolDefaultKey 的关联类型,这个关联类型遵守协议 RawRepresentable

// Setter
UserDefaults.set(true, forKey: .isUserLoggedIn)
// Getter
UserDefaults.bool(forKey: .isUserLoggedIn)

我们再一次挑战了只能使用已有 API 的规范,而定义了我们自己的 API 。这是因为,当我们扩展了 UserDefaults ,使用我们自己的 API 却丢失了上下文。如果这个 key 不是 .isUserLoggedIn ,我们还会理解它到底和什么关联么?

UserDefaults.set(true, forKey: .isAccepted) 
// Huh? isAccepted for what?

这个 key 的含义很模糊,它可能代表任何东西。即使看起来没什么,但提供上下文总是有好处的。

“有但是不需要”,比“不需要也没有”要好。

不用担心,添加上下文很简单。我们只需要给这个 key 添加一个命名空间。在这个例子中,我们创建了一个 Account 的命名空间,它包含了 isUserLoggedIn 这个 key

struct Account : BoolUserDefaultSettable {
    enum BoolDefaultKey : String {
        case isUserLoggedIn
    }
    ...
}
...
Account.set(true, forKey: .isUserLoggedIn)

冲突

ley account = Account.BoolDefaultKey.isUserLoggedIn.rawValue
let default = UserDefaults.BoolDefaultKey.isUserLoggedIn.rawValue
// account == default
// "isUserLoggedIn" == "isUserLoggedIn"

拥有两种分别遵守同一协议并提供了相同的 key 的类型绝对是有可能的,作为编程人员,如果我们不能在项目落地之前解决这个问题,那我们绝对要熬夜了。绝对不能冒着拿某个 key 改变另外一个 key 的值的风险。所以我们应该为我们自己的 key 创建命名空间。

命名空间

protocol KeyNamespaceable { }

我们肯定要为此创建一个协议了,谁叫咱们是 Swift 开发人员。协议通常是解决任何当前面临问题的首要尝试。如果协议是巧克力酱,我们就在所有的食物上面都抹上它,即使是牛排。知道我们有多爱协议了吗?

extension KeyNamespaceable { 
  func namespace<T>(_ key: T) -> String where T: RawRepresentable {
        return "\(Self.self).\(key.rawValue)"
  }
}

这是一个简单的方法,它将传入的字符串做了合并,并用"."来将这两个对象分开,一个是类的名字,一个是 keyRawValue 。我们也利用泛型来允许我们的方法接收一个遵守 RawRepresentable 协议的泛型参数 key

protocol BoolUserDefaultSettable : KeyNamespaceable

创建了命名空间协议之后,我们再来看之前的 BoolUserDefaultSettable 协议并让他遵守 KeyNamespaceable 协议,修改之前的扩展来让他发挥新功能的优势。

// BoolUserDefaultable extension
static func set(_ value: Bool, forKey key: BoolDefaultKey) {
    let key = namespace(key)
    UserDefaults.standard.set(value, forKey: key)
}
static func bool(forKey key: BoolDefaultKey) -> Bool {
    let key = namespace(key)
    return UserDefaults.standard.bool(forKey: key)
}
...
ley account = namespace(Account.BoolDefaultKey.isUserLoggedIn)
let default = namespace(UserDefaults.BoolDefaultKey.isUserLoggedIn)
// account != default
// "Account.isUserLoggedIn" != "UserDefaults.isUserLoggedIn"

上下文

由于创建了这个协议,我们可能会感觉从 UserDefaultsAPI 中解放了,也许会因此陶醉在协议的魅力之中。在这个过程中,我们通过将 key 移入有意义的命名空间来创建上下文。

Account.set(true, forKey: .isUserLoggedIn)

但由于这个 API 没有完整的意义我们还是一定程度上丢失了上下文。一眼看上去,代码中没有任何信息告诉我们这个布尔值会被持久存储。为了让一切圆满,我们准备扩展 UserDefaults 并把我们的默认类型放进去。

extension UserDefaults {
    struct Account : BoolUserDefaultSettable { ... }
}
...
UserDefaults.Account.set(true, forKey: .isUserLoggedIn)
UserDefaults.Account.bool(forKey: .isUserLoggedIn)

本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权,最新文章请访问 http://swift.gg

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • 泛字符串类型 API 缺点: 拼写错误 冲突 运行时崩溃 常见处理方法: 添加命名空间 不写字符串字面值 常见问题...
    coderzcj阅读 536评论 0 1
  • 原文在这里 纳尼? 如此简单的 UserDefaults 怎么去优雅的使用? 这么简单的还能玩出花来? 没毛病吧...
    跷脚啖牛肉阅读 38,233评论 19 94
  • 不如意事常八九,可与言者无二三。
    样儿r阅读 103评论 1 1
  • 好像很多人都把自己的健康交给了医生,但是不知道这有多危险,前天和一位医生朋友交流,他是一名外科医生自己刚做了...
    健康顾问飞雁阅读 212评论 0 0