使用AWS构建后端(三) —— Data Store(一)
版本记录
版本号 | 时间 |
---|---|
V1.0 | 2020.11.17 星期二 |
前言
使用
Amazon Web Services(AWS)
为iOS应用构建后端,可以用来学习很多东西。下面我们就一起学习下如何使用Xcode Server
。感兴趣的可以看下面几篇文章。
1. 使用AWS构建后端(一) —— Authentication & API(一)
2. 使用AWS构建后端(二) —— Authentication & API(二)
开始
首先看下主要内容:
在本教程中,您将扩展上一教程中的
Isolation Nation
应用程序,使用AWS Pinpoint
和AWS Amplify DataStore
添加分析和实时聊天功能。内容来自翻译。
下面看下写作环境:
Swift 5, iOS 14, Xcode 12
接着就是正文啦。
在本教程中,您将选择在Part 1, Authentication & API结尾处停下来的Isolation Nation
应用。 您将使用AWS Pinpoint
和AWS Amplify DataStore
扩展应用程序,以添加分析和实时聊天功能。
在您开始之前,请登录到AWS Console。
在项目的Amplify
控制台中,选择Backend environments
选项卡,单击Edit backend
链接,然后单击Copy
拉命令。
导航到终端中的启动程序项目,然后粘贴刚从Amplify
控制台复制的命令。 出现提示时,请选择您之前设置的配置文件(如果适用)。 选择no default editor
,一个iOS应用程序,然后从终端的选项列表中选择Y
来修改后端。
接下来,通过运行以下命令来生成Amplify配置文件:
amplify-app --platform ios
在Xcode
中打开IsolationNation.xcworkspace
文件,然后编辑amplifytools.xcconfig
文件以反映以下设置:
push=true
modelgen=true
profile=default
envName=dev
注意:打开正确文件的快速简便方法是输入以下命令:
xed .
最后,在终端中运行以下命令:
pod update
更新完成后,构建您的应用程序。您可能需要构建两次,因为Amplify
第一次需要生成User
模型文件。
注意:尽管建议您先阅读上一教程,但这不是必需的。在开始之前,您必须在计算机上设置
AWS Amplify
。而且,您必须添加具有Cognito Auth
和带有用户模型的AppSync API
的新Amplify
项目。请参阅上一教程中的说明。
Isolation Nation
是一款针对因COVID-19
而自我隔离的人的应用程序。它使他们可以向当地社区的其他人寻求帮助。在上一教程的结尾,该应用程序使用AWS Cognito
允许用户注册并登录到该应用程序。它使用AWS AppSync
读取和写入有关用户的公共用户数据。
现在构建并运行。如果您已经完成了上一教程,请确认您仍然可以使用之前创建的用户帐户登录。如果您是从这里开始的,请注册一个新用户。
App Analytics
分析人们在现实生活中是如何使用你的应用程序的,这是创建一个伟大产品过程中的一个重要部分。AWS Pinpoint
是一项为您的应用程序提供分析和营销能力的服务。在本节中,您将学习如何记录用户行为,以便将来进行分析。
首先,在项目的根目录打开一个终端。使用Amplify CLI
为您的项目添加分析功能:
amplify add analytics
当出现提示时,选择Amazon Pinpoint
并按Enter
接受默认资源名。键入Y
接受推荐的授权默认值。
接下来,在Xcode
中打开工作区并打开Podfile
。在包含end
的行前插入以下代码:
pod 'AmplifyPlugins/AWSPinpointAnalyticsPlugin'
这就增加了AWS Pinpoint
插件作为你的应用程序的依赖。切换到你的终端并运行以下程序来安装插件:
pod install --repo-update
回到Xcode,打开AppDelegate.swift
,将Pinpoint
插件添加到Amplify
配置中。在application(_:didFinishLaunchingWithOptions:)
中,直接在调用Amplify.configure()
之前添加以下代码行:
try Amplify.add(plugin: AWSPinpointAnalyticsPlugin())
您的应用程序现在配置为发送分析数据到AWS Pinpoint
。
Tracking Users and Sessions
使用Amplify
跟踪用户会话非常简单。事实上,再简单不过了,因为你什么都不用做!只要安装插件就会自动记录应用程序打开和关闭的时间。
但是,为了真正有用,你应该在你的分析调用中添加user identification
。在Xcode中,打开AuthenticationService.swift
。在文件的最底部,添加以下扩展名:
// MARK: AWS Pinpoint
extension AuthenticationService {
// 1
func identifyUserToAnalyticsService(_ user: AuthUser) {
// 2
let userProfile = AnalyticsUserProfile(
name: user.username,
email: nil,
plan: nil,
location: nil,
properties: nil
)
// 3
Amplify.Analytics.identifyUser(user.userId, withProfile: userProfile)
}
}
在这段代码中,你做了几件事:
- 1) 首先,创建一个新方法
identifyUserToAnalyticsService(_:)
,它接受一个AuthUser
对象。 - 2) 然后,为用户创建一个
analytics user profile
。对于分析,您只关心用户名,所以您将其他可选字段设置为nil
。 - 3) 最后调用
identifyUser(_:withProfile:)
。传递用户ID和刚刚创建的用户配置文件。这将在AWS Pinpoint
中识别用户。
接下来,更新setUserSessionData(_:)
的方法签名,以接受一个可选的AuthUser
参数:
private func setUserSessionData(_ user: User?, authUser: AuthUser? = nil) {
将以下内容添加到该方法中的DispatchQueue
块的末尾:
if let authUser = authUser {
identifyUserToAnalyticsService(authUser)
}
现在,在两个地方更新对setUserSessionData(_:authUser:)
的调用。在signIn(as:identifiedBy:)
和 checkAuthSession()
结束时进行相同的更改:
setUserSessionData(user, authUser: authUser)
现在将authUser
传递到setUserSessionData
。这允许它调用identifyUserToAnalyticsService(_:)
。
构建和运行。与您的用户多次登录和退出,这样您就会在您的Pinpoint
分析中看到一些东西。
接下来,打开终端,输入以下内容:
amplify console analytics
这将在浏览器中打开一个Pinpoint console
,显示应用程序后端的Analytics overview
。
在默认情况下,Pinpoint
显示过去30天的汇总数据。就目前而言,几乎可以肯定,这一数字的平均值将为零。在标题下面,选择Last 30 days
。然后,在弹出框中,选择今天的日期作为时间段的开始和结束。点击离开弹出窗口关闭它,统计将刷新与今天的数据。
在左侧菜单中,选择Usage
。在显示活动端点和活动用户的框中,您应该看到一些非零值。如果计数仍然为零,不要担心——刷新数据可能需要15分钟。如果是这种情况,请继续学习本教程并在结束时再次检查。
到目前为止,这些分析已经足够了。是时候开始构建聊天功能了!
Updating Data in DynamoDB
您可能已经注意到,所有测试用户的位置Locations
列表中都有SW1A
位置。相反,你的应用程序需要询问人们住在哪里。遗憾的是,不是每个人都能住在白金汉宫!
打开HomeScreenViewModel.swift
。在文件的顶部,导入Amplify
库:
import Amplify
HomeScreenViewModel
发布一个名为userPostcodeState
的属性。这将一个可选String
包装在一个Loading
枚举中。
导航到fetchUser()
。请注意如何将userPostcodeState
设置为.loaded
,以及一个硬编码的相关值SW1A 1AA
。将这一行改为:
// 1
userPostcodeState = .loading(nil)
// 2
_ = Amplify.API.query(request: .get(User.self, byId: userID)) { [self] event in
// 3
DispatchQueue.main.async {
// 4
switch event {
case .failure(let error):
logger?.logError(error.localizedDescription)
userPostcodeState = .errored(error)
return
case .success(let result):
switch result {
case .failure(let resultError):
logger?.logError(resultError.localizedDescription)
userPostcodeState = .errored(resultError)
return
case .success(let user):
// 5
guard
let user = user,
let postcode = user.postcode
else {
userPostcodeState = .loaded(nil)
return
}
// 6
userPostcodeState = .loaded(postcode)
}
}
}
}
下面是这段代码的作用:
- 1) 首先,将
userPostcodeState
设置为loading
。 - 2) 然后,从
DynamoDB
获取用户。 - 3) 分派到主队列,因为您应该始终从主线程修改已发布的变量。
- 4) 用通常的方式检查错误。
- 5) 如果请求成功,检查用户模型是否有邮政编码设置。如果没有,将
userPostcodeState
设置为nil
。 - 6) 如果是,则将
userPostcodeState
设置为loaded
,并将用户的邮政编码作为关联值。
构建和运行。这一次,当您的测试用户登录时,应用程序将显示一个屏幕,邀请用户输入邮政编码。
如果你想知道这个应用程序是如何显示这个屏幕的,请查看HomeScreen.swift
。注意,如果postcode
为nil
,视图是如何呈现SetPostcodeView
的。
在Home group
中打开SetPostcodeView.swift
。这是一个相当简单的视图。TextField
收集用户的邮政编码。Button
要求view model
在单击时执行addPostCode
操作。
现在,再次打开HomeScreenViewModel.swift
。在文件的底部找到addPostCode(_:)
并写它的实现:
// 1
_ = Amplify.API.query(request: .get(User.self, byId: userID)) { [self] event in
DispatchQueue.main.async {
switch event {
case .failure(let error):
logger?.logError("Error occurred: \(error.localizedDescription )")
userPostcodeState = .errored(error)
case .success(let result):
switch result {
case .failure(let resultError):
logger?
.logError("Error occurred: \(resultError.localizedDescription )")
userPostcodeState = .errored(resultError)
case .success(let user):
guard var user = user else {
let error = IsolationNationError.noRecordReturnedFromAPI
userPostcodeState = .errored(error)
return
}
// 2
user.postcode = postcode
// 3 (Replace me later)
_ = Amplify.API.mutate(request: .update(user)) { event in
// 4
DispatchQueue.main.async {
switch event {
case .failure(let error):
logger?
.logError("Error occurred: \(error.localizedDescription )")
userPostcodeState = .errored(error)
case .success(let result):
switch result {
case .failure(let resultError):
logger?.logError(
"Error occurred: \(resultError.localizedDescription )")
userPostcodeState = .errored(resultError)
case .success(let savedUser):
// 5
userPostcodeState = .loaded(savedUser.postcode)
}
}
}
}
}
}
}
}
同样,这看起来有很多代码。但它的大部分只是检查是否成功的请求和处理错误,如果没有:
- 1) 你调用
Amplify.API.query
。以通常的方式通过ID查询请求用户。 - 2) 如果成功,您可以通过将
postcode
设置为用户输入的值来修改获取的用户模型。 - 3) 然后调用
Amplify.API.mutate
改变已存在的模型。 - 4) 您处理响应。然后再次切换到主线程并检查是否有
error
。 - 5) 如果成功,则将
userPostcodeState
设置为保存的值。
再次构建并运行。当视图显示以收集用户的邮政编码时,输入SW1A 1AA
并点击Update
。一秒钟后,应用程序将再次显示Locations
屏幕,SW1A thread
显示在列表中。
现在输入以下到您的终端:
amplify console api
当被询问时,选择GraphQL
。AWS AppSync
登录页面将在您的浏览器中打开。选择Data Sources
。单击User
表的链接,然后选择Items
选项卡。
选择刚刚为其添加邮政编码的用户的ID
。注意,postcode
字段现在出现在记录中。
为其他用户打开记录,注意该字段是空的。这是像DynamoDB
这样的键-值数据库的一个重要特性。它们允许灵活的schema
,这对于新应用程序的快速迭代非常有用。
在本节中,您已经添加了一个GraphQL schema
。您使用AWS AppSync
从该schema
声明式地生成后端。您还使用了AppSync
来读取和写入数据到底层的DynamoDB
。
Designing a Chat Data Model
到目前为止,你已经有了一个基于云登录的应用程序。它还将用户记录读写到基于云的数据库中。但这对用户来说并不令人兴奋,不是吗?
是时候解决这个问题了!在本教程的其余部分中,您将为您的应用程序设计和构建聊天特性。
打开schema.graphql
。在AmplifyConfig
组中。在文件底部添加以下Thread model
:
# 1
type Thread
@model
# 2
@key(
fields: ["location"],
name: "byLocation",
queryField: "ThreadByLocation")
{
id: ID!
name: String!
location: String!
# 3
messages: [Message] @connection(
name: "ThreadMessages",
sortField: "createdAt")
# 4
associated: [UserThread] @connection(keyName: "byThread", fields: ["id"])
createdAt: AWSDateTime!
}
运行整个模型,这是你要做的:
- 1) 定义一个
Thread
类型。使用@model
指令告诉AppSync
为这个模型创建一个DynamoDB
表。 - 2) 您添加了
@key
指令,该指令在DynamoDB
数据库中添加了一个自定义索引。在本例中,您指定希望能够查询Thread
。 - 3) 您可以向
Thread
模型添加messages
。messages
包含Message
类型的数组。您可以使用@connection
指令来指定Thread
及其Messages
之间的一对多连接。稍后您将了解更多相关信息。 - 4) 添加一个包含
UserThread
对象数组的associated
字段。要在AppSync
中支持多对多连接,您需要创建一个joining model
。UserThread
是支持用户和线程之间连接的joining model
。
接下来,为Message
类型添加类型定义:
type Message
@model
{
id: ID!
author: User! @connection(name: "UserMessages")
body: String!
thread: Thread @connection(name: "ThreadMessages")
replies: [Reply] @connection(name: "MessageReplies", sortField: "createdAt")
createdAt: AWSDateTime!
}
如您所料,Message
类型具有到author
的连接,类型为User
。它还拥有到Thread
的连接以及对该Message
的任何Replies
。注意,线程@connection
的名称与线程类型中提供的名称相匹配。
接下来,添加回复的定义:
type Reply
@model
{
id: ID!
author: User! @connection(name: "UserReplies")
body: String!
message: Message @connection(name: "MessageReplies")
createdAt: AWSDateTime!
}
这里没什么新东西!这与上面的Message
类似。
现在为我们的UserThread
类型添加模型:
type UserThread
@model
# 1
@key(name: "byUser", fields: ["userThreadUserId", "userThreadThreadId"])
@key(name: "byThread", fields: ["userThreadThreadId", "userThreadUserId"])
{
id: ID!
# 2
userThreadUserId: ID!
userThreadThreadId: ID!
# 3
user: User! @connection(fields: ["userThreadUserId"])
thread: Thread! @connection(fields: ["userThreadThreadId"])
createdAt: AWSDateTime!
}
当使用AppSync
创建多对多连接时,您不会直接在类型上创建连接。相反,您可以创建一个连接模型。为了你的加入模型工作,你必须提供以下几件事:
- 1) 您可以为模型的每一边标识一个密钥。
fields
数组中的第一个字段定义此键的hash key
,第二个字段是sort key
。 - 2) 对于连接中的每个类型,您可以指定一个
ID
字段来保存连接数据。 - 3) 还可以提供每种类型的字段。这个字段使用
@connection
指令来指定上面的ID
字段用于连接到类型。
最后,将以下连接添加到postcode
后的User
类型,这样您的用户将访问他们的数据:
threads: [UserThread] @connection(keyName: "byUser", fields: ["id"])
messages: [Message] @connection(name: "UserMessages")
replies: [Reply] @connection(name: "UserReplies")
构建和运行。这将需要一些时间,因为Amplify Tools
插件做了很多工作:
- 1) 它会注意到所有新的
GraphQL
类型。 - 2) 它为你生成
Swift
模型。 - 3) 它在云中更新
AppSync
和DynamoDB
。
构建完成后,查看您的AmplifyModels
组。它现在包含所有新类型的模型文件。
然后在浏览器中打开DynamoDB
选项卡,确认每种类型的表也存在。
您现在有了一个数据模型,它反映在您的代码和云中!
Amplify DataStore
在前面,您学习了如何使用Amplify API
通过AppSync
读取和写入数据。Amplify
还提供DataStore
。DataStore
是用于与云同步数据的更复杂的解决方案。
Amplify DataStore
的主要优点是它在移动设备上创建和管理一个本地数据库。DataStore
存储了从云端获取的所有模型数据,就在你的手机上!
这允许您在没有互联网连接的情况下查询和修改数据。当您的设备重新联机时,DataStore
将同步更改。这不仅允许离线访问,也意味着你的应用对用户来说更快捷。这是因为在UI中显示更新之前,您不必等待到服务器的往返。
用于与DataStore
交互的编程模型与Amplify API
的编程模型略有不同。在使用API时,可以确保返回的任何结果都是DynamoDB
中存储的最新结果。相比之下,DataStore
将立即返回本地结果!然后它发出一个请求来更新它在后台的本地缓存。如果要显示最新信息,代码必须订阅更新或再次查询缓存。
如果你想根据数据的存在与否做出决定,这使得Amplify API
成为一个更好的解决方案。例如,我是否应该显示邮政编码输入屏幕?但是DataStore
是提供丰富用户体验的更好的抽象。因此,应用程序中的聊天功能将使用DataStore
。
要开始使用DataStore
,打开Podfile
并添加依赖项:
pod 'AmplifyPlugins/AWSDataStorePlugin'
然后,在您的终端中,按正常方式安装:
pod install --repo-update
接下来,打开AppDelegate.swift
并定位application(_:didFinishLaunchingWithOptions:)
。在调用Amplify.configure()
之前添加以下配置代码:
try Amplify.add(plugin: AWSDataStorePlugin(modelRegistration: AmplifyModels()))
您现在已经在应用程序中安装了DataStore
!接下来,您将使用它在本地存储数据。
Writing Data to DataStore
Isolation Nation
允许住在彼此附近的人请求援助。当用户更改邮政编码时,应用程序需要检查该邮政编码区域是否已经存在Thread
。如果没有,它必须创建一个。然后,它必须将用户添加到Thread
中。
打开HomeScreenViewModel.swift
。在文件的底部,类的右括号内,添加以下方法:
// MARK: - Private functions
// 1
private func addUser(_ user: User, to thread: Thread) -> Future<String, Error> {
return Future { promise in
// 2
let userThread = UserThread(
user: user,
thread: thread,
createdAt: Temporal.DateTime.now())
// 3
Amplify.DataStore.save(userThread) { result in
// 4
switch result {
case .failure(let error):
promise(.failure(error))
case .success(let userThread):
promise(.success(userThread.id))
}
}
}
}
在这个方法中,你使用DataStore API
来保存一个新的UserThread
记录:
- 1) 首先,您接收
User
和Thread
模型并返回一个Future
。 - 2) 接下来,创建一个连接用户和线程的
UserThread
模型。 - 3) 您使用的是
Amplify.DataStore.save
API以保存用户线程。 - 4) 最后,在适当的情况下使用成功或失败来完成
promise
。
下面,添加另一个方法在DataStore
中创建一个新线程:
private func createThread(_ location: String) -> Future<Thread, Error> {
return Future { promise in
let thread = Thread(
name: location,
location: location,
createdAt: Temporal.DateTime.now())
Amplify.DataStore.save(thread) { result in
switch result {
case .failure(let error):
promise(.failure(error))
case .success(let thread):
promise(.success(thread))
}
}
}
}
这与前面的示例非常相似。
接下来,创建一个方法来获取或创建线程,基于位置:
private func fetchOrCreateThreadWithLocation(
location: String
) -> Future<Thread, Error> {
return Future { promise in
// 1
let threadHasLocation = Thread.keys.location == location
// 2
_ = Amplify.API.query(
request: .list(Thread.self, where: threadHasLocation)
) { [self] event in
switch event {
case .failure(let error):
logger?.logError("Error occurred: \(error.localizedDescription )")
promise(.failure(error))
case .success(let result):
switch result {
case .failure(let resultError):
logger?.logError(
"Error occurred: \(resultError.localizedDescription )")
promise(.failure(resultError))
case .success(let threads):
// 3
guard let thread = threads.first else {
// Need to create the Thread
// 4
_ = createThread(location).sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error): promise(.failure(error))
case .finished:
break
}
},
receiveValue: { thread in
promise(.success(thread))
}
)
return
}
// 5
promise(.success(thread))
}
}
}
}
}
下面是这段代码的作用:
- 1) 首先为查询线程构建谓词
(predicate)
。在本例中,您希望查询具有给定位置的线程。 - 2) 然后你使用
Amplify
。用于查询具有所提供位置的线程的API。这里使用的是Amplify API
,而不是数据存储。这是因为您想立即知道线程是否已经存在。注意,这种形式的query
API
接受上面的谓词作为第二个参数。 - 3) 在检查错误之后,您将检查从API返回的值。
- 4) 如果API没有返回线程,那么就使用前面编写的方法创建一个线程。
- 5) 否则,返回从API查询接收到的线程。
现在,添加最后一个方法:
// 1
private func addUser(_ user: User, to location: String) {
// 2
cancellable = fetchOrCreateThreadWithLocation(location: location)
.flatMap { thread in
// 3
return self.addUser(user, to: thread)
}
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { completion in
switch completion {
case .failure(let error):
self.userPostcodeState = .errored(error)
case .finished:
break
}
},
receiveValue: { _ in
// 4
self.userPostcodeState = .loaded(user.postcode)
}
)
}
在这里,您编排调用您刚刚创建的方法:
- 1) 您接收
User
和location
。 - 2) 你调用
fetchOrCreateThreadWithLocation(location:)
,它会返回一个thread
。 - 3) 然后调用
addUser(_:to:)
,它在数据存储中创建一个UserThread
行。 - 4) 最后,将
userPostcocdeState
设置为loaded
。
最后,您需要更新addPostCode()
以从邮政编码中提取位置,并使用它来调用addUser(_:to:)
。找到// 3 (Replace me later)
注释。删除mutate
调用,并将其替换为:
// 1
Amplify.DataStore.save(user) { [self] result in
DispatchQueue.main.async {
switch result {
case .failure(let error):
logger?.logError("Error occurred: \(error.localizedDescription )")
userPostcodeState = .errored(error)
case .success:
// Now we have a user, check to see if there is a Thread already created
// for their postcode. If not, create it.
// 2
guard let location = postcode.postcodeArea() else {
logger?.logError("""
Could not find location within postcode \
'\(String(describing: postcode))'. Aborting.
"""
)
userPostcodeState = .errored(
IsolationNationError.invalidPostcode
)
return
}
// 3
addUser(user, to: location)
}
}
}
下面是你正在做的事情:
- 1) 首先,使用
DataStore save API
在本地保存用户。 - 2) 处理错误后,检查邮政编码是否具有有效的邮政编码区域。
- 3) 然后使用前面编写的方法将用户添加到该位置。
在运行应用程序之前,在浏览器中打开DynamoDB
标签。找到您先前为测试用户设置的邮政编码。因为您当时没有创建线程,所以这些数据现在是危险的!要删除它,请单击字段左侧的灰色加号图标。然后单击Remove
。
构建和运行。因为你删除了邮政编码,应用程序会显示“enter postcode”
屏幕。输入与前面相同的邮政编码SW1A 1AA
,然后点击Update
。
您将看到Locations
屏幕,正确的位置显示在列表的顶部。
在浏览器中,转到DynamoDB
选项卡并打开User
表。刷新页面。单击您的用户的链接并确认确实设置了邮政编码。打开Thread
和UserThread
表并确认那里也有记录。
现在构建并在其他模拟器上运行。当出现提示时,输入与前面相同的邮政编码SW1A 1AA
。返回浏览器,确认已经为其他User
设置了邮政编码。您还应该看到另一个UserThread
记录,但没有新Thread
。
Loading Threads
你可能感觉不到,但你的聊天应用程序已经开始成型了!您的应用程序现在有:
- 通过身份验证的用户
- 用户位置
- 线程与正确的用户分配
- 数据存储在云,使用
DynamoDB
下一步是在Location
屏幕中为用户加载正确的线程。
打开ThreadsScreenViewModel.swift
。在文件的顶部,导入Amplify
:
import Amplify
然后,在文件的底部,添加以下扩展名:
// MARK: AWS Model to Model conversions
extension Thread {
func asModel() -> ThreadModel {
ThreadModel(id: id, name: name)
}
}
这个扩展提供了一个关于Amplify-generated Thread
的方法。它返回视图使用的view model
。这样就可以将Amplify-specific
的关注点从UI代码中移除!
接下来,删除fetchThreads()
及其硬编码线程的内容。将其替换为:
// 1
guard let loggedInUser = userSession.loggedInUser else {
return
}
let userID = loggedInUser.id
// 2
Amplify.DataStore.query(User.self, byId: userID) { [self] result in
switch result {
case .failure(let error):
logger?.logError("Error occurred: \(error.localizedDescription )")
threadListState = .errored(error)
return
case .success(let user):
// 3
guard let user = user else {
let error = IsolationNationError.unexpectedGraphQLData
logger?.logError("Error fetching user \(userID): \(error)")
threadListState = .errored(error)
return
}
// 4
guard let userThreads = user.threads else {
let error = IsolationNationError.unexpectedGraphQLData
logger?.logError("Error fetching threads for user \(userID): \(error)")
threadListState = .errored(error)
return
}
// 5
threadList = userThreads.map { $0.thread.asModel() }
threadListState = .loaded(threadList)
}
}
下面是你正在做的事情:
- 1) 检查已登录的用户。
- 2) 使用
DataStore
查询API
按ID
查询用户。 - 3) 检查
DataStore
中的error
之后,确认用户不是nil
。 - 4) 还要检查用户上的
userThreads
数组是否为nil
。 - 5) 最后,设置要显示的线程列表。然后,将发布的
threadListState
更新为loaded
。
构建和运行。确认Locations
列表仍然显示正确的thread
。
现在是时候开始在用户之间发送消息了!
注意:对于本教程的其余部分,您应该运行两个模拟器。它们在同一个
thread
中应该有不同的用户。
Sending Messages
这里的第一个任务与上面ThreadsScreenViewModel
中的更改类似。
打开MessagesScreenViewModel.swift
。在文件的顶部添加Amplify
导入:
import Amplify
在文件的底部,添加一个扩展名,在Amplify
模型和view model
之间进行转换:
// MARK: AWS Model to Model conversions
extension Message {
func asModel() -> MessageModel {
MessageModel(
id: id,
body: body,
authorName: author.username,
messageThreadId: thread?.id,
createdAt: createdAt.foundationDate
)
}
}
然后,删除fetchMessages()
的内容。一旦您可以创建真实的消息,您就不需要这些硬编码的消息了!用DataStore
中的正确query
替换内容:
// 1
Amplify.DataStore.query(Thread.self, byId: threadID) { [self] threadResult in
switch threadResult {
case .failure(let error):
logger?
.logError("Error fetching messages for thread \(threadID): \(error)")
messageListState = .errored(error)
return
case .success(let thread):
// 2
messageList = thread?.messages?.sorted { $0.createdAt < $1.createdAt }
.map({ $0.asModel() }) ?? []
// 3
messageListState = .loaded(messageList)
}
}
这就是你在这里所做的:
- 1) 首先,通过
Thread
的ID查询Thread
。 - 2) 检查
error
后,检索连接到thread
的消息。将它们映射到一个MessageModels
列表。使用DataStore API
很容易访问连接的对象。您只需访问它们 — 数据将根据需要从后端存储延迟加载。 - 3) 最后,将
messageListState
设置为loaded
。
构建和运行。点击该thread
以查看消息列表。现在列表是空的。
在屏幕的底部,有一个文本框,用户可以在这里输入他们的帮助请求。当用户点击Send
时,视图将在视图模型上调用perform(action:)
。这将请求转发给addMessage(input:)
。
还在MessagesScreenViewModel.swift
,添加以下实现到addMessage(input:)
:
// 1
guard let author = userSession.loggedInUser else {
return
}
// 2
Amplify.DataStore.query(Thread.self, byId: threadID) { [self] threadResult in
switch threadResult {
case .failure(let error):
logger?.logError("Error fetching thread \(threadID): \(error)")
messageListState = .errored(error)
return
case .success(let thread):
// 3
var newMessage = Message(
author: author,
body: input.body,
createdAt: Temporal.DateTime.now())
// 4
newMessage.thread = thread
// 5
Amplify.DataStore.save(newMessage) { saveResult in
switch saveResult {
case .failure(let error):
logger?.logError("Error saving message: \(error)")
messageListState = .errored(error)
case .success:
// 6
messageList.append(newMessage.asModel())
messageListState = .loaded(messageList)
return
}
}
}
}
这个实现看起来非常熟悉!这就是你正在做的:
- 1) 首先检查是否有一个登录的用户作为作者。
- 2) 然后,在数据存储中查询
thread
。 - 3) 接下来,使用来自
input
的值创建一个新消息。 - 4) 您将
thread
设置为newMessage
的所有者。 - 5) 将消息保存到数据存储区。
- 6) 最后,将消息追加到
view model
的messageList
并发布messageListState
以更新API
。
在两个模拟器上构建和运行,然后点击Messages
屏幕。在一个模拟器上创建一个新消息…好哇!一条消息出现在屏幕上。
在浏览器中,在DynamoDB
选项卡中打开Message
表。确认消息已保存到云上。
您的新消息会出现——但只出现在您用来创建它的模拟器上。在另一个模拟器上,单击back
,然后重新进入thread
。该消息现在将出现。很明显,这是可行的,但是对于一个聊天应用来说,这并不是实时的!
Subscribing to Messages
幸运的是,DataStore
支持GraphQL Subscriptions
,这是这类问题的完美解决方案。
打开MessagesScreenViewModel.swift
并定位subscribe()
。在这个方法之前,添加一个属性来存储一个AnyCancellable?
:
var fetchMessageSubscription: AnyCancellable?
接下来,添加subscription completion handler
:
private func subscriptionCompletionHandler(
completion: Subscribers.Completion<DataStoreError>
) {
if case .failure(let error) = completion {
logger?.logError("Error fetching messages for thread \(threadID): \(error)")
messageListState = .errored(error)
}
}
如果subscription completes
时出现错误,此代码将messageListState
设置为error
状态。
最后,向subscribe()
添加以下实现:
// 1
fetchMessageSubscription = Amplify.DataStore.publisher(for: Message.self)
// 2
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: subscriptionCompletionHandler) { [self] changes in
do {
// 3
let message = try changes.decodeModel(as: Message.self)
// 4
guard
let messageThreadID = message.thread?.id,
messageThreadID == threadID
else {
return
}
// 5
messageListState = .updating(messageList)
// 6
let isNewMessage = messageList.filter { $0.id == message.id }.isEmpty
if isNewMessage {
messageList.append(message.asModel())
}
// 7
messageListState = .loaded(messageList)
} catch {
logger?.logError("\(error.localizedDescription)")
messageListState = .errored(error)
}
}
以下是您如何实现您的消息订阅:
- 1) 您可以使用
DataStore
中的publisher API
侦听Message
模型的更改。无论何时从AppSync
接收到GraphQL
订阅,或者当对数据存储进行本地更改时,都会调用该API。 - 2) 订阅主队列上的发布服务器
(publisher)
。 - 3) 如果成功,则从更改响应中解码
Message
对象。 - 4) 你检查以确保这条消息是应用程序正在显示的同一线程。遗憾的是,
DataStore
目前不允许使用谓词(predicate)
设置订阅。 - 5) 将
messageListState
设置为update
,并将其发布到UI。 - 6) 您检查该消息是否是新的。如果是,则将其附加到
messageList
。 - 7) 最后,将
messageListState
更新为loaded
。
同样,在两个模拟器上构建和运行。点击两者上的消息列表,从其中一个发送消息。注意消息是如何立即出现在两个设备上的。
这是一个实时聊天应用程序!
Replying to Messages
回复消息所需的更改几乎与发送消息所需的更改相同。如果你想创建一个功能齐全的聊天应用程序,那么请继续阅读!您将很快地了解它,因为它与上面的代码非常相似。但如果你对学习更感兴趣,可以跳过这一部分。
打开RepliesScreenViewModel.swift
并导入文件顶部的Amplify
:
import Amplify
接下来,在底部添加模型转换代码作为扩展:
// MARK: AWS Model to Model conversions
extension Reply {
func asModel() -> ReplyModel {
return ReplyModel(
id: id,
body: body,
authorName: author.username,
messageId: message?.id,
createdAt: createdAt.foundationDate
)
}
}
用一个DataStore
查询替换fetchReplies()
中的stub
实现:
Amplify.DataStore
.query(Message.self, byId: messageID) { [self] messageResult in
switch messageResult {
case .failure(let error):
logger?.
logError("Error fetching replies for message \(messageID): \(error)")
replyListState = .errored(error)
return
case .success(let message):
self.message = message?.asModel()
replyList = message?.replies?.sorted { $0.createdAt < $1.createdAt }
.map({ $0.asModel() }) ?? []
replyListState = .loaded(replyList)
}
}
在addReply()
中,添加一个实现来创建一个回复:
guard let author = userSession.loggedInUser else {
return
}
Amplify.DataStore.query(Message.self, byId: messageID) { [self] messageResult in
switch messageResult {
case .failure(let error):
logger?.logError("Error fetching message \(messageID): \(error)")
replyListState = .errored(error)
return
case .success(let message):
var newReply = Reply(
author: author,
body: input.body,
createdAt: Temporal.DateTime.now())
newReply.message = message
Amplify.DataStore.save(newReply) { saveResult in
switch saveResult {
case .failure(let error):
logger?.logError("Error saving reply: \(error)")
replyListState = .errored(error)
case .success:
replyList.append(newReply.asModel())
replyListState = .loaded(replyList)
return
}
}
}
}
添加handling subscription
:
var fetchReplySubscription: AnyCancellable?
private func subscriptionCompletionHandler(
completion: Subscribers.Completion<DataStoreError>
) {
if case .failure(let error) = completion {
logger?.logError("Error fetching replies for message \(messageID): \(error)")
replyListState = .errored(error)
}
}
最后,实现subscribe()
fetchReplySubscription = Amplify.DataStore.publisher(for: Reply.self)
.receive(on: DispatchQueue.main)
.sink(receiveCompletion: subscriptionCompletionHandler) { [self] changes in
do {
let reply = try changes.decodeModel(as: Reply.self)
guard
let replyMessageID = reply.message?.id,
replyMessageID == messageID
else {
return
}
replyListState = .updating(replyList)
let isNewReply = replyList.filter { $0.id == reply.id }.isEmpty
if isNewReply {
replyList.append(reply.asModel())
}
replyListState = .loaded(replyList)
} catch {
logger?.logError("\(error.localizedDescription)")
replyListState = .errored(error)
}
}
哇,速度真快!
在两个模拟器上编译和运行。点击thread
查看消息,然后点击一条消息查看回复。在你的用户之间来回发送一些回复。他们相处得多好,不是很好吗?
恭喜你!你有一个工作的聊天应用程序!
在这个由两部分组成的系列教程中,您已经创建了一个使用AWS Amplify
作为后端功能完备的聊天应用程序。这里有一些文档链接,可以帮助你锁定在本教程中获得的知识:
您可以从Amplify Docs中了解有关Amplify
的更多信息。这些包括用于web
和Android
的库。如果你想给你的应用程序添加额外的功能,你可以考虑使用S3来保存静态数据,比如用户图片。或者您可以使用@auth
GraphQL directive指令向模型数据添加对象级或字段级身份验证。
后记
本篇主要讲述了
Data Store
,感兴趣的给个赞或者关注~~~