所在的位置: swift >> swift资源 >> SwiftUserDefaults协议

SwiftUserDefaults协议

译者:Yake;校对:pmst;定稿:CMB

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

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

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

UserDefaults

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

StringlytypedAPI

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

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

使用常量

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

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

分组常量

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

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

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

letisUserLoggedIn="isUserLoggedIn"添加上下文

structConstants{structAccountletisUserLoggedIn="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避免初始化

structConstants{structAccountletisUserLoggedIn="isUserLoggedIn"}privateinit(){}}

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

静态变量

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

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

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

finalclassname:Stringstaticname:String//finalclass==static使用枚举类型避免拼写错误

enumConstants{enumAccount:String{caseisUserLoggedIn}...}...UserDefaults.standard.set(true,forKey:Constants.Keys.isUserLoggedIn.rawValue)UserDefaults.standard.bool(forKey:Constants.Keys.isUserLoggedIn.rawValue)

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

你可能已经注意到了,我们已经创建了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

protocolBoolUserDefaultable{associatedTypeBoolDefaultKey:RawRepresentable}

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

扩展

extensionBoolUserDefaultablewhereBoolDefaultKey.RawValue==String{...}

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

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

UserDefault的Setter方法

//BoolUserDefaultableextensionstaticfuncset(_value:Bool,forKeykey:BoolDefaultKey){letkey=key.rawValueUserDefaults.standard.set(value,forKey:key)}staticfuncbool(forKeykey:BoolDefaultKey)-Bool{letkey=key.rawValuereturnUserDefaults.standard.bool(forKey:key)}

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

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

extensionUserDefaults:BoolUserDefaultSettable{enumBoolDefaultKey:String{caseisUserLoggedIn}}

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

//SetterUserDefaults.set(true,forKey:.isUserLoggedIn)//GetterUserDefaults.bool(forKey:.isUserLoggedIn)

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

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

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

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

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

structAccount:BoolUserDefaultSettable{enumBoolDefaultKey:String{caseisUserLoggedIn}...}...Account.set(true,forKey:.isUserLoggedIn)冲突

leyaccount=Account.BoolDefaultKey.isUserLoggedIn.rawValueletdefault=UserDefaults.BoolDefaultKey.isUserLoggedIn.rawValue//account==default//"isUserLoggedIn"=="isUserLoggedIn"

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

命名空间

protocolKeyNamespaceable{}

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

extensionKeyNamespaceable{funcnamespaceT(_key:T)-StringwhereT:RawRepresentable{return"\(Self.self).\(key.rawValue)"}}

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

protocolBoolUserDefaultSettable:KeyNamespaceable

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

//BoolUserDefaultableextensionstaticfuncset(_value:Bool,forKeykey:BoolDefaultKey){letkey=namespace(key)UserDefaults.standard.set(value,forKey:key)}staticfuncbool(forKeykey:BoolDefaultKey)-Bool{letkey=namespace(key)returnUserDefaults.standard.bool(forKey:key)}...leyaccount=namespace(Account.BoolDefaultKey.isUserLoggedIn)letdefault=namespace(UserDefaults.BoolDefaultKey.isUserLoggedIn)//account!=default//"Account.isUserLoggedIn"!="UserDefaults.isUserLoggedIn"上下文

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

Account.set(true,forKey:.isUserLoggedIn)

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

extensionUserDefaults{structAccount:BoolUserDefaultSettable{...}}...UserDefaults.Account.set(true,forKey:.isUserLoggedIn)UserDefaults.Account.bool(forKey:.isUserLoggedIn)

本文由SwiftGG翻译组翻译,已经获得作者翻译授权,最新文章请访问


转载请注明:http://www.aierlanlan.com/rzgz/588.html