Slug david cover 2

借助 Swift 的枚举来简化登录

只有一种登录方法的应用不仅限制了用户选择,而且通常还会导致负面评价的发生,但是使用多种登录选项的话往往会让系统更为复杂。David East 将向我们演示如何通过使用 Swift 的枚举,让视图控制器以及登录逻辑变得清晰明了、简单易懂。跟随他的本次演讲来感受枚举作为 Swift 中第一类型的强大力量吧,除此之外,还可以学习到如何搭建一个稳固的认证流程。


大家好,我是 David East, 就职于 Google 的 Firebase 团队,担任开发支持。在 Firebase 里面,我们针对认证做了大量的研究。在这里,我想向大家分享一下我们如何才能够搭建用户可信赖的认证过程,包括为什么我们需要取得用户信任,以及我们如何在代码中使用 Swift 枚举来实现这个功能。

1颗星引发的惨案 (00:54)

我的几位朋友做了一个很赞的名为 Etch 的键盘应用。它是第一款基于多任务手势的键盘,允许用户通过在键盘上绘制一些简单的图形,快速地访问某些服务并与之通信。我帮忙对这款应用进行了测试,觉得真是6得不行,在 App Store 上架的第一周就被评为最佳新应用了。我都忍不住试着想象全5星好评的结果出现了。

用户的反馈令人振奋:“很好玩,很新奇,虽然需要时间去适应”。接着,他们开始转而研究为应用添加 Facebook 登录,因为有一个1星评价说:“不允许用 Facebook 登录是一个懒惰、奇葩的需求,你们应该实现自己的登录方法,或者让用户自己选择”。介于此,我发现如果我们想得到用户的信任,那么我们需要给他们选择的自由。因为除非你的应用非常出名,否则不给用户通过 Facebook 登录的话,用户是非常难以信任你的登录是否安全的。

Overcast 这款播客应用就是一个绝佳的例子,它提供了非常丰富的登录选项。当你首次下载完该应用的时候,它们并不会试图窥探你的任何隐私。它们给了你简单的选择:“第一次登录;已有账户;我不想创建账户”。他们同样还在 FAQ 中解释了为什么你需要创建一个账户,以消除你的疑惑,此外他们还提供了简洁明了的隐私协议。这个应用已经有接近 7000 个评分,绝大多数都是5星好评,因为他们成功取得了用户的信任。对于 Etch 来说,当他们实现了邮箱登陆之后,那个1星差评也被取消了。因此,我们从中得到一个结论:我们必须要给用户提供选择,这样才能得到他们的信任

Swift 中的枚举 (07:17)

使用多个不同的 SDK 实现多登录选项是非常不容易的,但是 Swift 针对处理多选项有了一个非常棒的结构,那就是枚举。

Receive news and updates from Realm straight to your inbox

enum LoginProvider {
  case Facebook
  case Email
  case Google
  case Twitter
}

我们过去经常将枚举视作整数集合的包装,通常是为了类型安全而指定的,里面包含了一系列常用的值。在 Objective-C 或者 C 中,我们会看到枚举值被映射到0、1、2、3等等这些整数值。而在 Swift 中,就大不一样了:除非你自行映射,否则这些值是不会建立映射的。每一个枚举值本身都是一个完全值(fully-fledged value),拥有枚举所定义的类型。在我们今天的例子中,枚举值的类型将会是 LoginProvider。因为 Swift 中枚举是第一类型(first-class type),因此我们完全可以这样做。

枚举相关知识点 (08:12)

知识点1:行列值 (08:23)

行列值(raw value)是个炫酷的特性,它允许我们给每个枚举值存储一个实际值,并且这些值都是相同类型。

enum LoginProvider: String {
  case Facebook = "facebook"
  case Email = "email"
  case Google = "google"
  case Twitter = "twitter"
}

let provider = LoginProvider.init(rawValue: "email")

在这个例子中,行列值是字符串类型。我们将它们设置为唯一的字符串值,借此我们就能够通过行列值来初始化枚举。比如说,我们只需给定 “email” 字符串,这样就可以获取 LoginProvider 中的 Email 枚举值了。

知识点2:关联值 (08:57)

和行列值类似,关联值(associate value)允许我们通过枚举来存储类型,不过我们无需将类型定死,每个枚举值都可以实现自己的类型。

enum LoginProvider {
  case Facebook
  case Email: (String, String)
  case Google
  case Twitter
}

let provider = LoginProvider.Email("bob@bob.com", "pass")

因此,枚举关联值并不是预先填充了一个默认值,就如同行列值做的那样。相反,我们可以在任何时候对其值进行改变,可以看作是一个变量。在我们的社交登录的例子中,每个登录模块都需要不同类型的证书。比如说,由于 E-mail 登录需要邮箱地址和密码,因此,我们可以将这两个值存储为两个字符串,然后将它们设置到 LoginProvider.Email当中。这些关联值可以是任意一种类型,并不会受到像行列值那样仅能够使用 String、Int、Char 的限定。

不过比起处理两个字符串来说,使用 LoginUser 这样一个结构体无疑是更佳的选择,通过设置这个结构体中的两个属性,我们还可以在其中添加验证邮箱地址和密码的函数。

struct LoginUser {
  let email: String
  let password: String
  func isValid() -> Bool {
    return email != "" && password != ""
  }
}

现在,我们可以使用 case Email: (LoginUser) 来存储 LoginUser,而不是像之前那样存储两个字符串可。接下来,我可以像下面这样将用户登录和邮箱登陆关联起来:

let provider = LoginProvider.Email(user)

要提取这些值的话,我们可以使用对枚举使用 switchcase 语句来选择,我们可以通过 case let 或者 case var 将值放入其中。在这些 case 语句中,我们还可以访问到用户的相关信息,从而进行登录操作。

switch provider {
  case let .Email(user)
    // login!
    break
}

知识点3:case where (10:43)

有了关于关联值的相关知识,知识点3关于 case where 的内容就变得容易理解了。之前,我们使用 case let 来将已经存储的 email 地址提取出来。现在通过 where 语句,我们可以通过调用我们之前创建的函数来验证用户是否合法。这将允许我们更专注于实际逻辑的实现:

switch provider {
  case let .Email(user) where user.isValid():
    // login!
    break
  case let .Email(user) where !user.isValid():
    // don’t login!
    break
}

我们可以检查到用户合法、用户不合法的两种情况,并分别对其进行处理。这种功能十分强大,之前我们只能在 case 语句中核查,现在我们有了更清晰直观的表达方式了。

知识点4 - 函数 (11:30)

由于在 Swift 中枚举是第一值类型,因此枚举中可以包含函数。当我们使用 switch 的时候,通过函数就可以匹配到 self,也就是当前枚举值。

enum LoginProvider {
  func login() {
    switch self {
    case let .Email(user) where user.isValid():
      // login!
      break
    }
  }
}

当枚举值是 .Facebook 的时候,self 就会是 .Facebook,不过这个例子中我们使用的是 .Email,由你所见匹配仍然完美实现。要使用它的话,我们只需要创建该枚举的一个实例,然后调用此方法即可:

let provider = LoginProvider.Facebook
provider.login()

// 我们如何登录到 View Controller 那里?

在这两行代码中,我们一定程度上将社交登录的多种类型提炼了出来。不过不幸的是,我们现在必须要面临这样一个事实:这个模型代码并没有将用户返回到 View Controller 那里的概念,因为登录操作是异步进行的。当我们支持多登录方式的时候,我们就在处理多条不同的异步流。

社交登录 SDK (12:37)

社交登录 SDK 就如同雪花一般:美丽却独一无二。

Social login SDKs are like snowflakes: they may be beautiful, but no two are the same:

  • Facebook - 异步登录,异步获取用户信息
  • Google - 需要两个代理方法(其中之一是 UI 代理)
  • Twitter - 异步获取账户信息,异步登录
  • Email & Password - 完全自定义

简化异步数据流 (13:14)

与其创建其他的登录服务模型,不如自行决定数据流,从而让这些数据流适应我们的要求。尽管代理并不像 Swifty 在和视图控制器交互时那样十分好用。

我们可以创建一个协议 (LoginProviderDelegate),然后使用自己的代理语法 (虽然并不简洁,但是能用就行)。我们可以创建一个方法,当用户登录后或者发生错误后调用。由于社交登录 SDK 的原因,我们可能要使用许多的代理方法,但是我们只需要在必要的时候再添加这些代理方法。

protocol LoginProviderDelegate {
  func loginProvider(loginProvider: LoginProvider, didSucceed user: User)
  func loginProvider(loginProvider: LoginProvider, didError error: NSError)
}

这个协议一般用于枚举中的那个 login(delegate:) 方法当中,这时候代理仅仅只是个参数。接下来我们获取 self 的类型,判断用户是否合法,然后调用登录方法,将用户模型传递进去,最后在服务器中异步回调的一个闭包中得到返回数据。

如果登录成功的话,我们就调用这个代理方法,它会连接到视图控制器上。这使得我们的视图控制器能够很轻易地读写我们的登录逻辑数据。

class ViewController: UIViewController, LoginProviderDelegate {
  let provider = LoginProvider.Facebook

  @IBAction func loginDidTouch() {
    provider.login(self)
  }

  func loginProvider(loginProvider: LoginProvider, didSucceed: User) {
    print(user)
  }

我们在此给视图控制器添加了 LoginProviderDelegate 协议,对于其他的登录方法我们也可以如法炮制。此外,如果拥有一个单击按钮的 IB 动作的话,我们还可以将视图控制器作为代理传递出去。当登录成功后,它就会调用登录模型肿的方法。这对所有的登录模型来说都是有效的。我们可以将这种实现应用到所有的数据流中,这样就不用担心某个方法未被实现。

代码示例 (15:03)

通过代理来为所有登录模型自定义数据流还有很大一步要走,通过它我们切实简化了我们了登录视图控制器,并且还降低了复杂登录代码的耦合度。因此,我们通过这种方法将所有的登录 SDK 实现细节和回调通过自己的简洁模型给替代掉了。

总而言之,我们通过给用户提供多种不同的登录选项以取得他们的信任,并且通过 Swift 的枚举我们还可以简化这些选项的实现。谢谢大家!

问&答 (18:55)

问:如何判断何时该使用枚举来实现多个登录选项,而何时该使用协议?应该怎么处理它们之间的平衡点呢?

David:我们的做法就是尽可能靠近 Swifty 的做法。在过去,我们对登录做了充分的包装,然后使用协议来进行处理。使用协议的话您的视图控制器就会比以前更加简洁,但是由于多登录选项对定义了多选项枚举的支持性,因此使用枚举也是可以的。协议是实现多登录选项的另一种选择,我对此并不固执,协议也是一个极佳的想法。

问:由于您同时关注了协议和枚举,那么在这次演讲中哪一个是您的核心呢?您是鼓励尽可能使用枚举还是鼓励搭建登录数据流呢?

David:两者都鼓励。我的主题是如何通过提供多种不同的认证登录形式来获取用户的信任,就像 Overcast 做的那样。因此,我很想和大家分享 Swift 枚举的美妙之处,以及其实现起来有多么简单。

问:之前我在我们的应用中实现枚举实现了登录数据流,并且发现过多关联值的枚举会导致构建数据流非常繁杂。为每个枚举使用一个自定义类型的对象是简化代码的好方法!

David: 没错,在为此次演讲准备的过程中我发现了这个问题,使用自定义类型的对象来构造的确是一个很好的注意。

About the content

This content has been published here with the express permission of the author.

David East

David East is a Developer Advocate at Google working on the Firebase & Angular teams. He’s also the co-author of AngularFire2. He takes full advantage of Google’s free coffee perk. ☕️

4 design patterns for a RESTless mobile integration »

close