Swift 傻瓜技巧:自定义枚举的映射关系

作者:Wooji Juice,原文链接,原文日期:2016-04-08
译者:Lanford3_3;校对:shanks;定稿:CMB

在 iOS 平台的 app 中,有种列表设计非常常见: 一个 table view, 里面是一个项目列表,每个项目只有一行简单的文字,对于被选中的项目,后面会有一个对勾。

当然,往往有着许多更好的方式来实现这种列表的功能,特别是在你的 app 中 UI 处于核心地位的时候。但是,用户们熟悉这种列表的展示方式,比如 App 中的设置页面,使用这种列表来表现是非常实用的。实际上,iOS 系统的设置页面也随处可见这种列表。让我们来为这篇文章找个案例--点开系统的设置,在其中点击信息一栏,然后点击保留信息,之后你就可以选择信息保留的时限:“30 天”,“一年” 或是 “永久”。

有时候选项是动态的(比如说,选择默认日历,里面的选项取决于用户创建了哪些日历),但是大部分时候,包括保留信息这个例子,选项列表是固定的,从代码的角度考虑,这些设置可以用一个枚举(enum)很好地表示出来:

enum KeepMessagesOptions: Int
{
    case For30Days, For1Year, Forever
}

通过让这个枚举基于 Int 类型,枚举中的每一项都会和一个索引关联起来(0 对应 For30Days,1 对应 For1Year,2 对应 Forever),这样的话,你就能将 NSIndexPath.row 和枚举互相转换了。

但是,为了存储设置项,你还需要一些别的东西:一个 永久的 ID, 和索引区分开来。为什么呢?让我们假设下次升级的时候你决定加入更多的时间间隔:

enum KeepMessagesOptions: Int
{
    case For30Days, For6Months, For1Year, Forever
}

如果之前一个 iOS 9 的用户选择了永久保存他的信息,而你也用了 index(2) 去存储他的设置,问题就产生了--现在这个索引对应的是 For1Year,所以用户可能会失去许多他本想保留下来的信息,这样很不好。

当然,你更不能用屏幕上显示的那些名称来存储设置,因为这些名称 a)(国际化时)要翻译成别的语言 b) 有可能有拼写错误,并在之后得到了修正(这样修正前和修正后的项目就无法对应起来了),或是发生一些类似的其他情况。通常来说,你永远都不该使用用户界面上的那些字符串来做设置的存储。所以你需要一些特殊的、机器能读取的 ID 来做这事儿。

这个问题显而易见,为什么我要一再强调呢?因为不得不费心神去处理这事儿,确实让人觉得很烦躁。

在 Objective-C 以及 C/C++ 中,枚举只是一个略漂亮的定义整数的方式。 所以,为了做一个 UI 给用户进行选项的选择,你需要定义两个字符串列表来表示这些选项(一个是给机器读取用的 ID 列表,一个是本地化的给人类阅读的名称列表),做一个 table view controller 来展示这些选项,每次你使用它的时候,传入人类可理解的字符串, 定义一些 API 来处理结果,每次读写设置项目的时候,你都需要处理所有索引、机器可读取的 ID 和供人类阅读的名称之间的转换。

这并不难。但这很乏味。

一种更好的方式

在 Swift 中,枚举能做的事多了不少。让我们快速地定义一个协议:

protocol PickableEnum
{
    var displayName: String { get }
    var permanentID: String { get }
    static var allValues: [Self] { get }
    static func fromPermanentID(id: String) -> Self?
}

这样一来,写一个常见的,能从任何 PickableEnum 中进行选项选择的 view controller 会变得相当简单了。 加到 Futures 中,你就能像下面这样轻松使用它:

@IBAction func showChoicesForKeepMessage()
{
    // 假设我们已经从别的地方拿到了 currentValue
    EnumPickerController.pickFromEnum(currentValue, from: self).onSuccess
    {
        newValue in
        // 处理 newValue, 把 newValue.permanentID 存到 NSUserDefaults 的什么地方去
    }

}

这看起来相当不错,但是枚举们本身怎样了呢--我们是否只是在枚举们实现 PickableEnum 这个协议中的方法时,把所有要做的额外工作都写在里面了呢?不 -- 枚举们由协议拓展(protocol extension)来实现:

extension PickableEnum where Self: RawRepresentable, Self.RawValue == Int
{
    var displayName: String
    {
        return Localised("\(self.dynamicType).\(self)")
    }
    
    var permanentID: String
    {
        return String(self)
    }
    
    static var allValues: [Self]
    {
        var result: [Self] = []
        var value = 0
        while let item = Self(rawValue: value)
        {
            result.append(item)
            value += 1
        }
        return result
    }
    
    static func fromPermanentID(id: String) -> Self?
    {
        return allValues.indexOf { $0.permanentID == id }.flatMap { self.init(rawValue: $0) }
    }
}

这些只用写一次,现在对于协议的所有要求,我们都有了合理的默认实现。所以我们所要做的一切就是让我们的枚举遵循协议:

enum KeepMessagesOptions: Int, PickableEnum
{
    case For30Days, For6Months, For1Year, Forever
}

当然,你也可以(对协议)使用你自己的实现。比如说,如果你需要兼容 NSUserDefaults 中已有的设置的 ID, 你可以这样做:

enum KeepMessagesOptions: Int, PickableEnum
{
    case For30Days, For6Months, For1Year, Forever
    var permanentID: String { return ["30d", "6m", "1y", "forever"][rawValue] }
}

但你也不是非得这么做。

另外,对于供人阅读的字符串,把他们加到本地化字符串文件(localised strings file), 用枚举类型和枚举的 case 作为 key, 你所需要做的就是这样:

在 Localized.strings 中:

"KeepMessagesOptions.For30Days" = "For 30 Days";
// etc

(我做了个假设,上面的那个 Localised() 是一个函数,它使用标准的 cocoa 本地化步骤,从 main bundle 中寻找适当的文本。为什么不用 NSLocalizedString() 呢?因为那只是用于静态的字符串,当你构造字符串(就像我们这儿所做的),运行 genstrings 或是类似的其他方法时,很可能给你带来麻烦。)

其他方法以及补充说明

另一种可能的方法是,使用基于 String 而非 Int 的枚举。 这样做的好处在于,你能够使用默认的 init(rawValue:) 方法以及 rawValue 成员变量来转换 ID 和 case,如果你通过拓展标准库中的 RawRepresentable 协议来实现 PickableEnum 协议,那么协议也可以缩减为仅剩 allValuesdisplayName

protocol PickableEnum: RawRepresentable
{
    var displayName: String { get }
    static var allValues: [Self] { get }
}

理论看起来比较简单,但是在实践中,你必须给枚举的 case 做索引来和 table view 的行进行相互转换。你还必须根据 case 手动地定义 allValues 数组。如果你发现你总是在做这事儿,也许使用一个基于 String 的枚举会更好。或者说,在将来,即使是对于基于非整形类型的枚举,Swift 也可能提供一种方法来自动地访问一个枚举中所有可能的 case(至少对于没有关联数据的枚举来说)。毕竟编译器掌握了相关信息[1]

但在我的例子中,我还是坚持用基于整形的枚举,这是出于另外一个原因:当我需要用到 Ferrite 的音频引擎(用 Objective-C++ 写的,之前已经提到过)中的值时,基于整形的枚举会让我更加容易对这些值进行处理。

另外一个小小的原因:我上面给出的是最简单的定义,但是在实践中,我发现如果 allValues 返回的不是一个实例数组,而是一个元组数组,比如说 [(name: String, id: String)],将会非常有用。这是 view controller 真正需要的数据,并且减少了泛型适配所有类型的副作用 -- 它只需要处理已知的类型。这点或许对你有价值。

噢,这还有另外一点……实际上,我们并不一定要用枚举来做这事儿。还记得不,最开始的时候我说了有时候列表是基于用户提供的数据的,并不是一个固定的集合?你可以构建自己的结构体来遵循 PickableEnum,你的 view controller (或者是其他你使用的组件,也许是……UIPickableView?)也能够很好地接受他。而且幸运的是,你不用去写一堆重复的代码。

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


  1. 就像你使用 switch 语句时,它知道你都有哪些 case

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

推荐阅读更多精彩内容