SwiftUI框架详细解析 (四) —— 使用SwiftUI进行苹果登录(一)

版本记录

版本号 时间
V1.0 2019.10.17 星期四

前言

今天翻阅苹果的API文档,发现多了一个框架SwiftUI,这里我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. SwiftUI框架详细解析 (一) —— 基本概览(一)
2. SwiftUI框架详细解析 (二) —— 基于SwiftUI的闪屏页的创建(一)
3. SwiftUI框架详细解析 (三) —— 基于SwiftUI的闪屏页的创建(二)

开始

主要内容:了解如何使用SwiftUI实施与Apple的登录,以为用户提供更多的隐私和对iOS应用程序的控制,本篇翻译来自地址

下面看下写作环境

Swift 5, iOS 13, Xcode 11

使用Apple登录(Sign In with Apple)iOS 13中的一项新功能,可以更快地在您的应用程序中注册和认证。

尽管Apple反复声明“与Apple登录”易于实现,但仍存在一些需要管理的地方。 在本教程中,您不仅将学习如何正确使用Apple实施登录,而且还将学习如何使用SwiftUI!

您需要Xcode 11,付费的Apple Developer会员身份和iOS 13设备,才能随本教程一起学习。

注意:您需要一台运行iOS 13的实际设备。模拟器并不总是能够正常运行。

由于您将在设备上运行并处理授权,因此请转到“项目”导航器,然后单击新的Signing & Capabilities标签,以设置团队标识符并适当更新包标识符。 如果您现在构建并运行该应用程序,则会看到正常的登录屏幕:

注意:您可以忽略Xcode显示的两个警告。 您将在教程中对其进行修复。


Add Capabilities

您的配置(PP)文件需要具有Sign In with Apple功能,因此请立即添加。 在项目导航器中单击项目,选择SignInWithApple目标,然后单击Signing & Capabilities选项卡。 最后,单击+ Capability,然后添加使用Apple登录功能。

如果您的应用程序具有关联的网站,则还应该添加Associated Domains功能。 此步骤是完全可选的,对于本教程或使用Apple登录功能来说不是必需的。 如果确实选择使用关联的域,请确保将“域” Domains值设置为字符串webcredentials :,后跟域名。 例如,您可以使用webcredentials:www.mydomain.com。 在本教程的后面,您将了解需要对网站进行的更改。


Add Sign In Button

Apple没有为Sign In with Apple按钮提供SwiftUI视图,因此您需要自己包装。 创建一个名为SignInWithApple.swift的新文件,然后粘贴此代码。

import SwiftUI
import AuthenticationServices

// 1
final class SignInWithApple: UIViewRepresentable {
  // 2
  func makeUIView(context: Context) -> ASAuthorizationAppleIDButton {
    // 3
    return ASAuthorizationAppleIDButton()
  }
  
  // 4
  func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) {
  }
}

这是正在做的事情:

  • 1) 需要包装UIView时,可以将UIViewRepresentable子类化。
  • 2) makeUIView应该始终返回特定类型的UIView
  • 3) 由于您没有执行任何自定义操作,因此直接返回“使用Apple登录”对象。
  • 4) 由于视图的状态永远不变,因此请保留一个空的实现。

现在您可以将按钮添加到SwiftUI,打开ContentView.swift并将其添加到UserAndPassword视图的正下方:

SignInWithApple()
  .frame(width: 280, height: 60)

Apple的样式指南要求最小尺寸为280×60,因此请务必遵循。 构建并运行您的应用程序,您应该会看到按钮!


Handle Button Taps

现在,点击按钮不会执行任何操作。 在设置按钮frame的位置下方,添加一个手势识别:

.onTapGesture(perform: showAppleLogin)

然后在body属性之后实现showAppleLogin()

private func showAppleLogin() {
  // 1
  let request = ASAuthorizationAppleIDProvider().createRequest()

  // 2
  request.requestedScopes = [.fullName, .email]

  // 3
  let controller = ASAuthorizationController(authorizationRequests: [request])    
}

这是您设置的内容:

  • 1) 所有登录请求都需要一个ASAuthorizationAppleIDRequest
  • 2) 指定您需要知道的最终用户数据的类型。
  • 3) 生成将显示登录对话框的控制器。

您应该仅请求所需的用户数据。 Apple为您生成一个用户ID。因此,如果获取电子邮件的唯一目的是拥有一个唯一的标识符,那么您就不需要它了,所以请不要请求。

1. ASAuthorizationControllerDelegate

当用户尝试进行身份验证时,Apple将调用两个委托方法之一,因此请立即实施。打开SignInWithAppleDelegates.swift。您将在此处实现代码,该代码将在用户点击按钮后运行。虽然您可以在自己的视图中实现代码,但将代码放置在其他位置以提高可重用性会更简洁。

您现在只需将authorizationController(controller:didCompleteWithError :)保留为空,但在生产应用中,应处理这些错误。

授权成功后,将调用authorizationController(controller:didCompleteWithAuthorization :)。您可以在下载的示例代码中看到两种情况。通过检查credential属性,可以确定用户是通过Apple ID还是通过存储的iCloud密码进行了身份验证。

传递给委托方法的ASAuthorization对象包括您要求的任何属性,例如电子邮件或名称。该值的存在是您如何确定这是新注册还是现有登录名的方式。

注意:Apple只会在首次身份验证时为您提供所需的详细信息。

记住前面的注释至关重要! Apple假设您将存储所需的详细信息,因此不再需要它们。这是您需要处理的“与Apple登录”的地方之一。

考虑用户首次登录的情况,因此您需要执行注册。 Apple向您提供用户的电子邮件和全名。然后,您尝试调用您的服务器注册代码。除非您的服务器不在线或设备的网络连接断开等。

用户下次登录时,Apple不会提供详细信息,因为它希望您已经拥有这些详细信息。这将导致existing user流运行,从而导致失败。

2. Handling Registration

authorizationController(controller:didCompleteWithAuthorization :)的第一个case语句中,添加以下内容:

// 1
if let _ = appleIdCredential.email, let _ = appleIdCredential.fullName {
  // 2
  registerNewAccount(credential: appleIdCredential)
} else {
  // 3
  signInWithExistingAccount(credential: appleIdCredential)
}

在此代码中:

  • 1) 如果您收到详细信息,则表明它是新注册。
  • 2) 收到详细信息后,请调用您的注册方法。
  • 3) 如果您没有收到详细信息,请调用您现有的帐户方法。

将以下注册方法粘贴到扩展名的顶部:

private func registerNewAccount(credential: ASAuthorizationAppleIDCredential) {
  // 1
  let userData = UserData(email: credential.email!,
                          name: credential.fullName!,
                          identifier: credential.user)

  // 2
  let keychain = UserDataKeychain()
  do {
    try keychain.store(userData)
  } catch {
    self.signInSucceeded(false)
  }

  // 3
  do {
    let success = try WebApi.Register(
      user: userData,
      identityToken: credential.identityToken,
      authorizationCode: credential.authorizationCode
    )
    self.signInSucceeded(success)
  } catch {
    self.signInSucceeded(false)
  }
}

这里做了一些事情:

  • 1) 将所需的详细信息和Apple提供的user保存在结构体中。
  • 2) 将详细信息存储到iCloud钥匙串中以备后用。
  • 3) 调用您的服务,并向调用者表明注册是否成功。

请注意credential.user的用法。此属性包含Apple分配给最终用户的唯一标识符。当您将此用户存储在服务器上时,请使用此值(而不是电子邮件或登录名)。提供的值将在用户拥有的所有设备上完全匹配。此外,Apple将为与您的Team ID相关联的所有应用程序为用户提供相同的值。用户运行的任何应用均会收到相同的ID,这意味着您已经在服务器上拥有了他们的所有信息,无需要求用户提供它!

您服务器的数据库可能已经为用户存储了其他标识符。只需在您的user类型表中添加一个新列,其中包含Apple提供的标识符。然后,您的服务器端代码将首先检查该列是否匹配。如果未找到,请还原为现有的登录或注册流程,例如使用电子邮件地址或登录名。

根据服务器处理安全性的方式,您可能会或可能不需要发送credential.identityTokencredential.authorizationCodeOAuth流使用这两部分数据。 OAuth设置超出了本教程的范围。

注意:Apple提供了使用OAuth详细信息生成其公钥所需的数据。本质上,他们为您提供了JSON Web Key (JWK)

为了确保在钥匙串中正确存储,请在CredentialStorage中编辑UserDataKeychain.swift并更新account以获取应用程序的包标识符,然后附加任何其他文本值。我喜欢在包标识符后附加.Details。重要的是,account属性和包标识符不完全匹配,因此存储的值仅用于您打算使用的目的。

3. Handling Existing Accounts

如前所述,现有用户登录到您的应用程序时,Apple不会提供电子邮件和全名。在SignInWithAppleDelegates.swift中的注册方法下面添加此方法以处理这种情况:

private func signInWithExistingAccount(credential: ASAuthorizationAppleIDCredential) {
  // You *should* have a fully registered account here.  If you get back an error
  // from your server that the account doesn't exist, you can look in the keychain 
  // for the credentials and rerun setup

  // if (WebAPI.login(credential.user, 
  //                  credential.identityToken,
  //                  credential.authorizationCode)) {
  //   ...
  // }

  self.signInSucceeded(true)
}

您在此方法中放置的代码将非常针对应用程序。 如果您从服务器收到失败消息,告知您该用户尚未注册,则应使用retrieve()查询钥匙串。 使用返回的UserData结构的详细信息,然后重新尝试注册最终用户。

4. Username and Password

与Apple一起使用登录时,另一种可能性是最终用户将选择已存储在该站点的iCloud钥匙串中的凭据credentials。 在authorizationController(controller:didCompleteWithAuthorization :)的第二种case语句中,添加以下行:

signInWithUserAndPassword(credential: passwordCredential)

然后在signInWithExistingAccount(credential :)下面,实现适当的方法:

private func signInWithUserAndPassword(credential: ASPasswordCredential) {
  // if (WebAPI.login(credential.user, credential.password)) {
  //   ...
  // }
  self.signInSucceeded(true)
}

同样,您的实现将针对特定应用。 但是,您需要调用服务器登录名并传递用户名和密码。 如果服务器无法识别用户,则您将需要运行完整的注册流程,因为您不具备钥匙串中提供的有关其电子邮件和姓名的任何详细信息。


Finish Handling Button Press

返回ContentView.swift,您需要一个属性来存储刚创建的代理。 在类的顶部,添加以下代码行:

@State var appleSignInDelegates: SignInWithAppleDelegates! = nil

@State是告诉SwiftUI的结构将拥有其拥有并更新的可变内容。 所有@State属性必须具有一个实际值,这就是为什么出现nil奇怪的赋值的原因。

现在,在同一文件中,用以下命令替换controller创建,以完成showAppleLogin()

// 1
appleSignInDelegates = SignInWithAppleDelegates() { success in
  if success {
    // update UI 
  } else {
    // show the user an error
  }
}

// 2
let controller = ASAuthorizationController(authorizationRequests: [request])
controller.delegate = appleSignInDelegates

// 3
controller.performRequests()

这是正在做的事情:

  • 1) 生成代理并将其分配给类的属性。
  • 2) 像以前一样生成ASAuthorizationController,但是这次,告诉它使用您的自定义委托类。
  • 3) 通过调用performRequests(),您要求iOS显示Sign In with Apple模式视图。

代理的回调是根据最终用户是否成功通过您的应用进行身份验证来处理任何必要的展示更改的地方。


Automate Sign In

您已经实现了使用Apple登录的功能,但是用户必须明确点击该按钮。 如果已将他们带到登录页面,则应查看他们是否已配置Sign In with Apple。 返回ContentView.swift,将此行添加到.onAppear块中:

 self.performExistingAccountSetupFlows()

注意:SwiftUI.onAppear {}UIKitviewDidAppear(_ :)基本相同。

当视图出现时,您希望iOS同时检查Apple IDiCloud钥匙串以查找与此应用程序相关的凭据。 如果存在,则将自动显示Sign In with Apple对话框,因此用户不必手动按下按钮。 由于同时按下按钮和自动调用都共享代码,因此将showAppleLogin方法重构为两种方法:

private func showAppleLogin() {
  let request = ASAuthorizationAppleIDProvider().createRequest()
  request.requestedScopes = [.fullName, .email]
  
  performSignIn(using: [request])
}

private func performSignIn(using requests: [ASAuthorizationRequest]) {
  appleSignInDelegates = SignInWithAppleDelegates() { success in
    if success {
      // update UI
    } else {
      // show the user an error
    }
  }

  let controller = ASAuthorizationController(authorizationRequests: requests)
  controller.delegate = appleSignInDelegates

  controller.performRequests()
}

除了将代理的创建和显示移至其自己的方法之外,没有任何代码更改。

现在,实现performExistingAccountSetupFlows()

private func performExistingAccountSetupFlows() {
  // 1
  #if !targetEnvironment(simulator)

  // 2
  let requests = [
    ASAuthorizationAppleIDProvider().createRequest(),
    ASAuthorizationPasswordProvider().createRequest()
  ]

  // 2
  performSignIn(using: requests)
  #endif
}

这里只有几个步骤:

  • 1) 如果您使用的是模拟器,则什么也不做。如果您进行这些调用,模拟器将打印出错误。
  • 2) 要求Apple提出对Apple IDiCloud钥匙串检查的请求。
  • 3) 调用您现有的安装代码。

请注意,在第2步中,您没有指定要检索哪些最终用户详细信息。回想一下本教程的前面部分,您在该教程中了解到,仅一次提供了详细信息。由于此流程用于检查现有帐户,因此无需指定requestedScopes属性。实际上,如果您在此处设置了它,它将被忽略!


Web Credentials

如果您有专门针对您的应用程序的网站,则可以走做的更多,也可以处理Web凭据。如果您查看UserAndPassword.swift,则会看到对SharedWebCredential(domain :)的调用,该调用当前向构造函数发送一个空字符串。将其替换为您的网站域。

现在,登录到您的网站,并在网站的根目录下创建一个名为.well-known的目录。在其中创建一个名为apple-app-site-association的新文件,并粘贴以下JSON

{
    "webcredentials": {
        "apps": [ "ABCDEFGHIJ.com.raywenderlich.SignInWithApple" ]
    }
}

注意:确保文件名没有扩展名。

您需要将ABCDEFGHIJ替换为团队的10个字符的Team ID。 您可以在Membership选项卡下的https://developer.apple.com/account中找到您的Team ID。 您还需要使bundle identifier与您用于该应用程序的标识符匹配。

通过执行这些步骤,您已将Safari存储的登录详细信息与应用程序的登录详细信息相关联。 它们现在可用于Sign in with Apple

当用户手动输入用户名和密码时,将存储凭据,以便以后使用。


Runtime Checks

在应用程序生命周期内的任何时候,用户都可以进入设备设置并为您的应用程序禁用Sign In with Apple。 您将根据要执行的操作检查它们是否仍在登录。Apple建议您运行以下代码:

let provider = ASAuthorizationAppleIDProvider()
provider.getCredentialState(forUserID: "currentUserIdentifier") { state, error in
  switch state {
  case .authorized:
    // Credentials are valid.
    break
  case .revoked:
    // Credential revoked, log them out
    break
  case .notFound:
    // Credentials not found, show login UI
    break
  }
}

苹果公司表示,getCredentialState(forUserId :)调用非常快。 因此,您应该在应用启动期间以及需要确保用户仍然通过身份验证的任何时间运行它。 我建议您除非必须,否则不要在应用程序启动时运行。 您的应用真的需要登录或注册用户才能使用吗? 在他们尝试执行实际上需要登录的操作之前,无需他们登录。实际上,甚至Human Interface Guidelines也建议这样做!

请记住,如果要求他们做的第一件事是注册,那么许多用户将卸载刚刚下载的应用程序。

相反,请监听Apple提供的通知以了解用户何时登出。 只需侦听ASAuthorizationAppleIDProvider.credentialRevokedNotification通知并采取适当的措施。


The UIWindow

至此,您已经完全实现了Sign In with Apple。 恭喜你!

如果您观看了有关使用Apple登录的WWDC演示文稿,或者已阅读其他教程,则可能会发现这里缺少一块。 您从未实现过ASAuthorizationControllerPresentationContextProviding代理方法来告诉iOS使用哪个UIWindow。 从技术上讲,如果您使用默认的UIWindow并不是必需的,那么最好知道如何处理。

如果您不使用SwiftUI,那么从SceneDelegate中获取window属性并返回值非常简单。 在SwiftUI中,它变得更加困难。


The Environment

SwiftUI的新概念是Environment。 在该区域中,您可以存储需要用于许多SwiftUI视图的数据。 在某种程度上,您可以将其视为依赖注入。

1. Environment Setup

看一下EnvironmentWindowKey.swift,您将看到在SwiftUI环境中存储值所需的代码。 它是样板代码,其中您定义了要传递给@Environment属性包装器的键以及要存储的值。 请注意,由于存储了类的类型,因此如何将其显式标记为weak引用以防止循环引用。

注意:环境代码是由WWDC实验室中的Apple工程师提供的

2. ContentView Changes

跳回到ContentView.swift并在ContentView的顶部添加另一个属性:

@Environment(\.window) var window: UIWindow?

iOS会自动使用存储在环境中的值填充window变量。

performSignIn(using :)中,修改构造函数调用以传递window属性:

appleSignInDelegates = SignInWithAppleDelegates(window: window) { success in

您还需要告诉ASAuthorizationController将您的类用于presentationContextProvider代理,因此请在分配controller.delegate后立即添加以下代码。

controller.presentationContextProvider = appleSignInDelegates

3. Update the Delegate

打开SignInWithAppleDelegates.swift处理新属性,并对类进行构造函数更改。 使用以下所有类的注册和委托方法而不是扩展替换类定义:

class SignInWithAppleDelegates: NSObject {
  private let signInSucceeded: (Bool) -> Void
  // 1
  private weak var window: UIWindow!

  // 2
  init(window: UIWindow?, onSignedIn: @escaping (Bool) -> Void) {
    // 3
    self.window = window
    self.signInSucceeded = onSignedIn
  }
}

只是一些更改:

  • 1) 存储新的对windowweak引用。
  • 2) 将UIWindow参数作为第一个参数添加到初始化程序。
  • 3) 将传入的值存储到属性。

最后,实现新的代理类型:

extension SignInWithAppleDelegates: 
    ASAuthorizationControllerPresentationContextProviding {
  func presentationAnchor(for controller: ASAuthorizationController) 
      -> ASPresentationAnchor {
    return self.window
  }
}

代理只有一种方法可以实现,该方法可以返回window,该窗口显示Sign In with Apple模式对话框。

4. Update the Scene

UIWindow放入环境是最后要做的事。 打开SceneDelegate.swift并替换以下行:

window.rootViewController = UIHostingController(rootView: ContentView())

用下面内容

// 1
let rootView = ContentView().environment(\.window, window)

// 2
window.rootViewController = UIHostingController(rootView: rootView)

只需两个小步骤即可完成所有操作:

  • 1) 您创建ContentView并附加window变量的值。
  • 2) 您将该rootView变量传递给UIHostingController而不是旧的初始化程序。

environment方法会返回some View,这基本上意味着它将使用您的ContentView,将您传递的值推入该视图的环境中,然后将ContentView返回给您。 现在,从ContentView呈现的任何SwiftUI View也会在其环境中保留该值。

如果您在其他任何地方创建新的根视图,则该根将不包含环境值,除非您也明确地将它们返回。


Logins Do not Scroll

使用Apple登录时要记住的一个缺点是,iOS显示的window不会滚动! 对于大多数用户而言,这无关紧要,但是请务必注意。 例如,作为我的应用程序使用的网站的所有者,我拥有许多登录名。 我不仅可以自己登录应用程序,而且还可以登录SQL数据库,PHP管理站点等。

如果您登录的次数过多,则最终用户可能看不到他们的实际需求。 尝试确保将应用程序链接到网站时,该网站仅具有对应用程序重要的登录信息。 不要将所有应用捆绑在一个域中。

后记

本篇主要讲述了使用SwiftUI进行苹果登录,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容