不管是从语言自己仍是项目代码,Swift3
的革新无疑是一场“惊天海啸” ,一些读者可能正奋战在代码迁移的前线。但即便有如此之多的改动, Swift
中依旧存在许多基于 Foundation
框架,泛字符串类型的 API
。这些 API
彻底没有问题,只是...github
咱们对这种 API
有一种既爱又恨的感情:偏心它的灵活性;又恨一时粗心致使问题接踵而来。这简直是在刀尖上编程。编程
Foundation
框架的开发者们之因此提供泛字符串类型的接口,是考虑到没法准确预见咱们将来会如何使用这个框架。这些开发者们极尽本身的智慧、能力和知识,最终决定在某些 API
中使用字符串,这为咱们开发人员带来了无尽的可能性,也能够说是一种黑魔法。swift
今天的主题是我学习 iOS 开发初期最早熟悉的 API
之一。对于那些不熟悉它的人来讲,它不过是对一系列信息的持久化存储,例如一张图片,一些应用的设置等。部分开发者偏向于认为它是"轻量级的 Core Data
。尽管人们绞尽脑汁想要把它做为替代品楔入,但结果代表它还远远不够强大。框架
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)
一种能够帮咱们维持一致性的模式就是将咱们全部重要的默认常量分组写在同一个地方。这里咱们建立了一个常量结构体来存储并指向咱们的默认值。this
还有一个建议是将你的属性名字设置成它对应的值,尤为是跟默认值打交道的时候。这样作能够简化你的代码并使属性在总体上有更好的一致性。拷贝属性名,将他们粘贴在字符串中,这样能够避免拼写错误。spa
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)
建立一个常量结构体彻底没有问题,可是在咱们写代码的时候记得提供上下文。咱们努力的目标是让本身的代码对任何人都具备较高的可读性,包括咱们本身。.net
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
第三定律。
UserDefault
的 Setter
方法// 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) }
是的,这是对标准 UserDefaults
的 API
的简单封装。咱们这么作是由于这样代码的可读性会更高,由于你只需传入简单的枚举值而不须要传入冗长的字符串(校对者注:摒弃相似下面 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)" } }
这是一个简单的方法,它将传入的字符串作了合并,并用"."来将这两个对象分开,一个是类的名字,一个是 key
的 RawValue
。咱们也利用泛型来容许咱们的方法接收一个遵照 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"
因为建立了这个协议,咱们可能会感受从 UserDefaults
的 API
中解放了,也许会所以陶醉在协议的魅力之中。在这个过程当中,咱们经过将 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。