美文网首页
使用AWS构建后端(三) —— Data Store(一)

使用AWS构建后端(三) —— Data Store(一)

作者: 刀客传奇 | 来源:发表于2020-11-17 18:43 被阅读0次

    版本记录

    版本号 时间
    V1.0 2020.11.17 星期二

    前言

    使用Amazon Web Services(AWS)为iOS应用构建后端,可以用来学习很多东西。下面我们就一起学习下如何使用Xcode Server。感兴趣的可以看下面几篇文章。
    1. 使用AWS构建后端(一) —— Authentication & API(一)
    2. 使用AWS构建后端(二) —— Authentication & API(二)

    开始

    首先看下主要内容:

    在本教程中,您将扩展上一教程中的Isolation Nation应用程序,使用AWS PinpointAWS Amplify DataStore添加分析和实时聊天功能。内容来自翻译

    下面看下写作环境:

    Swift 5, iOS 14, Xcode 12

    接着就是正文啦。

    在本教程中,您将选择在Part 1, Authentication & API结尾处停下来的Isolation Nation应用。 您将使用AWS PinpointAWS 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。注意,如果postcodenil,视图是如何呈现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
    

    当被询问时,选择GraphQLAWS 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模型添加messagesmessages包含Message类型的数组。您可以使用@connection指令来指定Thread及其Messages之间的一对多连接。稍后您将了解更多相关信息。
    • 4) 添加一个包含UserThread对象数组的associated字段。要在AppSync中支持多对多连接,您需要创建一个joining modelUserThread是支持用户和线程之间连接的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) 它在云中更新AppSyncDynamoDB

    构建完成后,查看您的AmplifyModels组。它现在包含所有新类型的模型文件。

    然后在浏览器中打开DynamoDB选项卡,确认每种类型的表也存在。

    您现在有了一个数据模型,它反映在您的代码和云中!


    Amplify DataStore

    在前面,您学习了如何使用Amplify API通过AppSync读取和写入数据。Amplify还提供DataStoreDataStore是用于与云同步数据的更复杂的解决方案。

    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) 首先,您接收UserThread模型并返回一个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) 您接收Userlocation
    • 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表。刷新页面。单击您的用户的链接并确认确实设置了邮政编码。打开ThreadUserThread表并确认那里也有记录。

    现在构建并在其他模拟器上运行。当出现提示时,输入与前面相同的邮政编码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查询APIID查询用户。
    • 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 modelmessageList并发布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的更多信息。这些包括用于webAndroid的库。如果你想给你的应用程序添加额外的功能,你可以考虑使用S3来保存静态数据,比如用户图片。或者您可以使用@auth GraphQL directive指令向模型数据添加对象级或字段级身份验证。

    后记

    本篇主要讲述了Data Store,感兴趣的给个赞或者关注~~~

    相关文章

      网友评论

          本文标题:使用AWS构建后端(三) —— Data Store(一)

          本文链接:https://www.haomeiwen.com/subject/ddlbbktx.html