- SwiftUI框架详细解析 (四) —— 使用SwiftUI进行
- SwiftUI框架详细解析 (十) —— 基于SwiftUI构建
- SwiftUI框架详细解析 (十三) —— 基于SwiftUI创
- SwiftUI框架详细解析 (十二) —— 基于SwiftUI创
- SwiftUI框架详细解析 (十一) —— 基于SwiftUI构
- SwiftUI框架详细解析 (二十) —— 基于SwiftUI的
- SwiftUI框架详细解析 (十六) —— 基于SwiftUI简
- SwiftUI框架详细解析 (十七) —— 基于SwiftUI简
- 在UIKit中使用SwiftUI(SwiftUI和UIKit混合
- SwiftUI:Swift项目(UIKit)中调用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.identityToken
和credential.authorizationCode
。 OAuth
流使用这两部分数据。 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 {}
与UIKit
的viewDidAppear(_ :)
基本相同。
当视图出现时,您希望iOS同时检查Apple ID
和iCloud
钥匙串以查找与此应用程序相关的凭据。 如果存在,则将自动显示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 ID
和iCloud
钥匙串检查的请求。 - 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) 存储新的对
window
的weak
引用。 - 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进行苹果登录,感兴趣的给个赞或者关注~~~
网友评论