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

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘贴.待开发的功能:1.支持自动生成约束2.开发设置页面3.做一个浏览器插件,支持不需要下载整个工程,可即时操作当前蓝湖浏览页面4.支持Flutter语言模板生成5.支持更多平台,如Sketch等6.支持用户自定义语言模板
现实生活中,我们听到的声音都是时间连续的,我们称为这种信号叫模拟信号。模拟信号需要进行数字化以后才能在计算机中使用。目前我们在计算机上进行音频播放都需要依赖于音频文件。那么音频文件如何生成的呢?音频文件的生成过程是将声音信息采样、量化和编码产生的数字信号的过程,我们人耳所能听到的声音频率范围为(20Hz~20KHz),因此音频文件格式的最大带宽是20KHZ。根据奈奎斯特的理论,音频文件的采样率一般在40~50KHZ之间。奈奎斯特采样定律,又称香农采样定律。...............
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿遍又亿遍,久久不能离开!看着小仙紫姐姐的蹦迪视频,除了一键三连还能做什么?突发奇想,能不能把舞蹈视频转成代码舞呢?说干就干,今天就手把手教大家如何把跳舞视频转成代码舞,跟着仙女姐姐一起蹦起来~视频来源:【紫颜】见过仙女蹦迪吗 【千盏】一、核心功能设计总体来说,我们需要分为以下几步完成:从B站上把小姐姐的视频下载下来对视频进行截取GIF,把截取的GIF通过ASCII Animator进行ASCII字符转换把转换的字符gif根据每
【Android App】实战项目之仿抖音的短视频分享App(附源码和演示视频 超详细必看)
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至2022年4月底。我已经将这篇博客的内容写为论文,上传至arxiv:https://arxiv.org/pdf/2204.10160.pdf欢迎大家指出我论文中的问题,特别是语法与用词问题在github上,我也上传了完整的项目:https://github.com/Whiffe/Custom-ava-dataset_Custom-Spatio-Temporally-Action-Video-Dataset关于自定义ava数据集,也是后台
因为我既对接过session、cookie,也对接过JWT,今年因为工作需要也对接了gtoken的2个版本,对这方面的理解还算深入。尤其是看到官方文档评论区又小伙伴表示看不懂,所以做了这期视频内容出来:视频在这里:本期内容对应B站的开源视频因为涉及的知识点比较多,视频内容比较长。如果你觉得看视频浪费时间,可以直接阅读源码:goframe v2版本集成gtokengoframe v1版本集成gtokengoframe v2版本集成jwtgoframe v2版本session登录官方调用示例文档jwt和sess
【Android App】实战项目之仿微信的私信和群聊App(附源码和演示视频 超详细必看)
用Android Studio的VideoView组件实现简单的本地视频播放器。本文将讲解如何使用Android视频播放器VideoView组件来播放本地视频和网络视频,实现起来还是比较简单的。VideoView组件的作用与ImageView类似,只是ImageView用于显示图片,VideoView用于播放视频。...
采用MATLAB对正弦信号,语音信号进行生成、采样和内插恢复,利用MATLAB工具箱对混杂噪声的音频信号进行滤波
随着移动互联网、云端存储等技术的快速发展,包含丰富信息的音频数据呈现几何级速率增长。这些海量数据在为人工分析带来困难的同时,也为音频认知、创新学习研究提供了数据基础。在本节中,我们通过构建生成模型来生成音频序列文件,从而进一步加深对序列数据处理问题的了解。
基于yolov5+deepsort+slowfast算法的视频实时行为检测。1. yolov5实现目标检测,确定目标坐标 2. deepsort实现目标跟踪,持续标注目标坐标 3. slowfast实现动作识别,并给出置信率 4. 用框持续框住目标,并将动作类别以及置信度显示在框上
数字电子钟设计本文主要完成数字电子钟的以下功能1、计时功能(24小时)2、秒表功能(一个按键实现开始暂停,另一个按键实现清零功能)3、闹钟功能(设置闹钟以及到时响10秒)4、校时功能5、其他功能(清零、加速、星期、八位数码管显示等)前排提示:前面几篇文章介绍过的内容就不详细介绍了,可以看我专栏的前几篇文章。PS.工程文件放在最后面总体设计本次设计主要是在前一篇文章 数字电子钟基本功能的实现 的基础上改编而成的,主要结构不变,分频器将50MHz分为较低的频率备用;dig_select
1.进入官网下载OBS stdioOpen Broadcaster Software | OBS (obsproject.com)2.下载一个插件,拓展OBS的虚拟摄像头功能链接:OBS 虚拟摄像头插件.zip_免费高速下载|百度网盘-分享无限制 (baidu.com)提取码:6656--来自百度网盘超级会员V1的分享**注意**该插件必须下载但OBS的根目录(应该是自动匹配了的)3.打开OBS,选中虚拟摄像头选择启用在底部添加一段视频录制选择下面,进行录制.
Meta公司在9月29日首次推出一款人工智能系统模型:Make-A-Video,可以从给定的文字提示生成短视频。基于**文本到图像生成技术的最新进展**,该技术旨在实现文本到视频的生成,可以仅用几个单词或几行文本生成异想天开、独一无二的视频,将无限的想象力带入生活
音频信号叠加噪声及滤波一、前言二、信号分析及加噪三、滤波去噪四、总结一、前言之前一直对硬件上的内容比较关注,但是可能是因为硬件方面的东西可能真的是比较杂,而且需要渗透的东西太多了,所以学习进展比较缓慢。因为也很少有单纯的硬件学习研究,总是会伴随着各种理论需要硬件做支撑,所以还是想要慢慢接触理论学习。但是之前总找不到切入点,不知道从哪里开始,就一直拖着。最近稍微接触了一点信号处理,就用这个当作切入点,开始接触理论学习。二、信号分析及加噪信号处理选用了matlab做工具,选了一个最简单的语音信号处理方
腾讯云 TRTC 实时音视频服务体验,从认识 TRTC 到 TRTC 的开发实践,Demo 演示& IM 服务搭建。
音乐音频分类技术能够基于音乐内容为音乐添加类别标签,在音乐资源的高效组织、检索和推荐等相关方面的研究和应用具有重要意义。传统的音乐分类方法大量使用了人工设计的声学特征,特征的设计需要音乐领域的知识,不同分类任务的特征往往并不通用。深度学习的出现给更好地解决音乐分类问题提供了新的思路,本文对基于深度学习的音乐音频分类方法进行了研究。首先将音乐的音频信号转换成声谱作为统一表示,避免了手工选取特征存在的问题,然后基于一维卷积构建了一种音乐分类模型。
C++知识精讲16 | 井字棋游戏(配资源+视频)【赋源码,双人对战】
本文主要讲解如何在Java中,使用FFmpeg进行视频的帧读取,并最终合并成Gif动态图。
在本篇博文中,我们谈及了 Swift 中 some、any 关键字以及主关联类型(primary associated types)的前世今生,并由浅及深用简明的示例向大家讲解了它们之间的奥秘玄机。