SwiftUI框架详细解析 (十四) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(一)

版本记录

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

前言

今天翻阅苹果的API文档,发现多了一个框架SwiftUI,这里我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. SwiftUI框架详细解析 (一) —— 基本概览(一)
2. SwiftUI框架详细解析 (二) —— 基于SwiftUI的闪屏页的创建(一)
3. SwiftUI框架详细解析 (三) —— 基于SwiftUI的闪屏页的创建(二)
4. SwiftUI框架详细解析 (四) —— 使用SwiftUI进行苹果登录(一)
5. SwiftUI框架详细解析 (五) —— 使用SwiftUI进行苹果登录(二)
6. SwiftUI框架详细解析 (六) —— 基于SwiftUI的导航的实现(一)
7. SwiftUI框架详细解析 (七) —— 基于SwiftUI的导航的实现(二)
8. SwiftUI框架详细解析 (八) —— 基于SwiftUI的动画的实现(一)
9. SwiftUI框架详细解析 (九) —— 基于SwiftUI的动画的实现(二)
10. SwiftUI框架详细解析 (十) —— 基于SwiftUI构建各种自定义图表(一)
11. SwiftUI框架详细解析 (十一) —— 基于SwiftUI构建各种自定义图表(二)
12. SwiftUI框架详细解析 (十二) —— 基于SwiftUI创建Mind-Map UI(一)
13. SwiftUI框架详细解析 (十三) —— 基于SwiftUI创建Mind-Map UI(二)

开始

首先看下主要内容:

在本教程中,您将学习如何使用Firebase Cloud Firestore将持久性添加到SwiftUI iOS应用程序。内容来自翻译

接着看下主要内容:

Swift 5, iOS 14, Xcode 12

接着就是主要内容:

Google的移动后端服务,即Firebase,为应用程序开发人员提供了从分析到分发(Analytics to Distribution),再到数据库和身份验证(Databases and Authentication)的所有功能。在本教程中,您将了解Cloud Firestore(该服务套件的一部分)以及如何将其与SwiftUI结合使用。

Cloud Firestore是一个灵活的NoSQL云数据库,开发人员可以使用它实时存储和同步应用程序数据。您将使用它为FireCard提供数据和服务,该应用程序可通过创建卡片来帮助用户记住概念。

在此过程中,您将学习如何:

  • Set up Firestore.
  • Use MVVM to structure an scalable code base.
  • Manipulate data using Combine and Firestore.
  • Use anonymous authentication.

打开入门项目。

Firecards是一个简单的工具,可让用户通过提供问题和答案来创建卡片。稍后,他们可以通过阅读问题并轻按卡片上的内容来测试自己的记忆力,以查看其答案是否正确。

目前,该应用没有持久性存储(persistence),因此用户无法对其做太多事情。但是您可以通过将Firestore添加到此SwiftUI应用中来解决此问题。

您将不会使用它几个步骤,而是在Xcode中打开FireCards.xcodeproj

注意:该项目使用Swift Package Manager来管理依赖项。由于Firebase SDK很大,因此建议您在阅读本教程的同时在后台打开项目,并让其获取并解决所有依赖项。

1. Setting Up Firebase

您必须先创建一个Firebase帐户,然后才能使用Cloud Firestore。转到Firebase网站。在右上角,单击Go to console。然后提供您的Google帐户凭据(Google account credentials);如果您还没有凭据,请创建一个。

接下来,单击+ Add project。将会出现一个modal,询问您的项目名称。类型FireCard

下一步要求您为项目启用Google Analytics(分析)。 由于本教程没有介绍Analytics(分析),因此请单击底部的切换按钮将其禁用。 然后单击Create project

几秒钟后,您会看到一条消息,上面写着Your new project is ready。 点击Continue,您将看到项目的仪表板:

您可以在此处访问所有Firebase服务。 选择Add an app to get started的圆圈iOS按钮开始使用。 在iOS Bundle ID字段中输入com.raywenderlich.firecards,然后单击Register app

按照概述的说明下载GoogleService-Info.plist并将其拖到FireCards Xcode项目中:

Xcode提示时,请确保选中Copy Items if needed

下一步要求您将Firebase SDK添加到您的iOS应用中,这已经为您完成。 单击Next转到Add initialization code步骤。

打开AppDelegate.swift。 通过添加以下导入语句,确保包括Firebase

import Firebase

接下来,将此代码添加到application(_:didFinishLaunchingWithOptions :)中的return语句之前:

FirebaseApp.configure()

Firebase项目的网页上,单击Next,然后单击Continue to console

这会将您带回到项目的概述页面:

您完成了Firebase的设置,并为您的应用授予了访问所有Firebase服务的权限。 接下来,您将配置Cloud Firestore

2. Setting Up Cloud Firestore

在左侧菜单的Develop下,单击Cloud Firestore。 然后,单击Create database

将出现一个modal,向您显示下一步:

  • Cloud Firestore设置安全规则。
  • 设置Cloud Firestore位置。

Firebase使用安全规则来处理数据访问授权。 选择Start in test mode,这将使您的数据在未经授权的情况下可访问30天。

尽管这对于测试项目是可以的,但是您应该始终设置适当的Security Rules。 幸运的是,您稍后将在Adding Authorization Using Security Rules中进行介绍。

点击Next。 现在,助手将询问您要将数据存储在何处。

请注意,该位置可能会影响您的帐单,以后将无法更改。 现在,选择nam5(us-central),然后单击Enable

现在,Firebase将为您配置所有内容。 然后,它将把您转到Firebase项目的Cloud Firestore部分:

在这里,您将了解如何实时插入,删除或更新数据。 您也可以根据需要手动操作它。


Architecting the App Using MVVM

对于此项目,您将使用Model-View-View ModelMVVM来构建应用程序的组件。

MVVM是一种结构设计模式,它将构成应用程序的元素分为Views, View Models and Models。 这种设计模式有助于开发人员从视图中分离业务逻辑,并保持必要的关注点分离,以使视图和模型与数据源和业务逻辑不可知。

由于您将Cloud Firestore用于数据持久性,因此将添加一个层来处理与数据源进行交互所需的逻辑。 对于此项目,您将使用存储库模式。 下图显示了应用程序体系结构的最终表示形式:

  • Models保存应用程序数据。它们代表了您的应用需要管理的实体。
  • Views构成构成应用程序的视觉元素,并负责显示模型中数据。
  • View Model通过转换模型中的数据使其可以显示在视图中,从而使模型与视图之间的关系成为可能。
  • Repository表示处理数据源data source通信的抽象。在这种情况下,数据源是Cloud Firestore。当View Model需要对数据进行任何操作时,它会与Repository进行通信,并通知有关数据更改的视图。

1. Thinking in Collections and Documents

Cloud FirestoreNoSQL数据库。它使用集合和文档(collections and document)来构造数据。

集合保存文档。这些文档documents的字段构成了您应用程序的实体,在本例中为卡片。因此,卡片是文档,卡片组是集合。

这是应用程序数据结构的直观表示:

您可以编写查询(queries)以从集合中获取数据,或插入,更新或删除文档。 为此,您需要使用唯一标识符创建对集合或特定文档的引用。 创建新文档时,您可以手动传递此标识符,否则Cloud Firestore会为您创建一个。

聊够了,该写代码了!

2. Adding New Cards

首先创建Repository以访问数据。

在项目导航器中,右键单击Repositories,然后单击New file…。 创建一个名为CardRepository.swift的新Swift文件,并向其中添加以下代码:

// 1
import FirebaseFirestore
import FirebaseFirestoreSwift
import Combine 

// 2
class CardRepository: ObservableObject {
  // 3
  private let path: String = "cards"
  // 4
  private let store = Firestore.firestore()

  // 5
  func add(_ card: Card) {
    do {
      // 6
      _ = try store.collection(path).addDocument(from: card)
    } catch {
      fatalError("Unable to add card: \(error.localizedDescription).")
    }
  }
}

在这里:

  • 1) 导入FirebaseFirestore,FirebaseFirestoreSwiftCombineFirebaseFirestore使您可以访问Firestore API,而Combine为Swift提供了一组声明性API。

FirebaseFirestoreSwift添加了一些很酷的功能来帮助您将Firestore与模型集成。它使您可以将Cards转换为文档,并将文档转换为Cards

  • 2) 定义CardRepository并使遵循ObservableObjectObservableObject使用发布者帮助此类发出更改,因此其他对象可以侦听并做出相应的反应。
  • 3) 然后,声明path并分配cards的值。这是Firestore中的集合名称。
  • 4) 声明store并分配对Firestore实例的引用。
  • 5) 接下来,定义add(_ :)并使用do-catch块捕获由代码引发的任何错误。如果在更新文档时出了点问题,则会因致命错误终止应用的执行。
  • 6) 使用path创建对cards集合的引用,然后将card传递到addDocument(from:encoder:completion :)。这会将新卡添加到集合中。

使用上面的代码,编译器将报addDocument(from:encoder:completion :)要求Card符合Encodable。要解决此问题,请打开Card.swift并将类定义更改为此:

struct Card: Identifiable, Codable {

通过添加CodableSwift可以无缝地对Cards进行序列化和反序列化。 其中包括导致错误的Encodable和将Firestore文档转换为Swift对象时将使用的Decodable

3. Adding the View Model

您需要一个视图模型才能将模型与视图连接。 在Project导航器的ViewModels组下,创建一个名为CardListViewModel.swift的新Swift文件。

将此添加到新文件:

// 1
import Combine

// 2
class CardListViewModel: ObservableObject {
  // 3
  @Published var cardRepository = CardRepository()

  // 4
  func add(_ card: Card) {
    cardRepository.add(card)
  }
}

下面进行细分:

  • 1) Combine为您提供了处理异步代码的API。
  • 2) 您声明CardListViewModel并使它符合ObservableObject。 这使您可以侦听此类型对象发出的更改。
  • 3) @Published为此属性创建一个发布者,以便您可以订阅它。
  • 4) 您将card传递到存储库,以便可以将其添加到集合中。

打开NewCardForm.swift并为您创建的view model添加一个属性,紧随NewCardForm中的其他属性之后:

@ObservedObject var cardListViewModel: CardListViewModel

之前的更改将使Xcode Preview停止工作,因为现在NewCardForm需要一个CardListViewModel。 要解决此问题,请更新NewCardForm_Previews

static var previews: some View {
  NewCardForm(cardListViewModel: CardListViewModel())
}

NewCardForm的底部添加以下addCard()方法:

private func addCard() {
  // 1
  let card = Card(question: question, answer: answer)
  // 2  
  cardListViewModel.add(card)
  // 3
  presentationMode.wrappedValue.dismiss()
}

这段代码:

  • 1) 使用已经在顶部声明的questionanswer属性创建Card
  • 2) 使用view model添加新card
  • 3) 关闭当前视图。

然后,通过将Button(action:{}){替换为下面代码,将此新方法称为Add New Card的操作。

Button(action: addCard) {

最后,打开CardListView.swift,找到.sheet修饰符,并通过现在传递一个新的视图模型实例来修复编译器错误。 您稍后将使用共享实例。

.sheet(isPresented: $showForm) {
  NewCardForm(cardListViewModel: CardListViewModel())
}

构建并运行。

点击右上角的+。 填写question and answer字段,然后点击Add New Card

嗯,什么都没发生。卡片未显示在主屏幕中:

在网络浏览器中打开Firebase Console,然后转到Cloud Firestore部分。 Firestore自动创建了cards收藏。 单击标识符以导航到新文档:

您的数据存储在Firebase中,但您仍未实现检索和显示卡片的逻辑。


Retrieving and Displaying Cards

现在该展示您的卡片了! 首先,您需要创建一个view model来代表一张Card

在项目导航器的ViewModels下,创建一个名为CardViewModel.swift的新Swift文件。

将此添加到新文件:

import Combine

// 1
class CardViewModel: ObservableObject, Identifiable {
  // 2
  private let cardRepository = CardRepository()
  @Published var card: Card
  // 3
  private var cancellables: Set<AnyCancellable> = []
  // 4
  var id = ""

  init(card: Card) {
    self.card = card
    // 5
    $card
      .compactMap { $0.id }
      .assign(to: \.id, on: self)
      .store(in: &cancellables)
  }
}

在这里:

  • 1) 声明CardViewModel并使它与ObservableObject兼容,以便它可以发出更改和Identifiable,从而保证您可以迭代CardViewModels的数组。
  • 2) 这为实际的card模型提供了引用。 @Published为此属性创建一个发布者,以便您可以订阅它。
  • 3) cancellables用于存储您的订阅,因此您以后可以取消订阅。
  • 4) id是要求遵循Identifiable的属性。 它应该是唯一的标识符。
  • 5) 在卡的ID和视图模型的ID之间为card设置绑定。 然后将对象存储在cancellables对象中,以便以后可以取消。

1. Setting Up the Repository

您的存储库需要处理获取卡的逻辑。 打开CardRepository.swift并在属性定义下方的顶部添加以下代码:

// 1
@Published var cards: [Card] = []

// 2
init() {
  get()
}

func get() {
  // 3
  store.collection(path)
    .addSnapshotListener { querySnapshot, error in
      // 4
      if let error = error {
        print("Error getting cards: \(error.localizedDescription)")
        return
      }

      // 5
      self.cards = querySnapshot?.documents.compactMap { document in
        // 6
        try? document.data(as: Card.self)
      } ?? []
    }
}

在上面的代码中,您:

  • 1) 定义cards@Published为此属性创建一个发布者,以便您可以订阅它。 每次修改此数组时,所有侦听者都会做出相应的反应。
  • 2) 创建初始化方法并调用get()
  • 3) 使用path获取对集合根目录的引用,并添加一个侦听器以接收集合中的更改。
  • 4) 检查是否发生错误,打印错误消息并返回。
  • 5) 在querySnapshot.documents上使用compactMap(_ :)遍历所有元素。 如果querySnapshotnil,则改为设置一个空数组。
  • 6) 使用data(as:decoder :)将每个文档映射为Card。 您可以这样做,这要归功于您在顶部导入的FirebaseFirestoreSwift,并且Card符合Codable

2. Setting Up CardListViewModel

接下来,打开CardListViewModel.swift并将这两个属性添加到CardListViewModel

// 1
@Published var cardViewModels: [CardViewModel] = []
// 2
private var cancellables: Set<AnyCancellable> = []

在此代码中,您:

  • 1) 使用@Published属性包装器定义cardViewModels,以便您可以订阅它。 它将包含CardViewModels数组。
  • 2) 创建一组AnyCancellables。 它将用于存储您的订阅,以便您以后可以取消订阅。

仍在视图模型中时,添加以下初始化程序:

init() {
  // 1
  cardRepository.$cards.map { cards in
    cards.map(CardViewModel.init)
  }
  // 2
  .assign(to: \.cardViewModels, on: self)
  // 3
  .store(in: &cancellables)
}

您添加的代码:

  • 1) 监听cards,并将数组的每个Card元素映射到CardViewModel中。 这将创建一个CardViewModels数组。
  • 2) 将前一个映射操作的结果分配给cardViewModels
  • 3) 将此预订的实例存储在cancellables的对象中,以便在取消初始化CardListViewModel时自动将其取消。

3. Setting Up CardView

打开CardView.swift并进行以下更改。

var card: Card替换为:

var cardViewModel: CardViewModel

这使视图可以直接使用视图模型而不是Card模型。

然后,在frontView中,将card.question替换为:

cardViewModel.card.question

接下来,在backView中,将card.answer替换为:

cardViewModel.card.answer

最后,将CardView_Previews更改为此:

struct CardView_Previews: PreviewProvider {
  static var previews: some View {
    let card = testData[0]
    return CardView(cardViewModel: CardViewModel(card: card))
  }
}

进行了这些更改后,您现在可以直接传递预期的CardViewModel而不是Card模型。 但是,您需要再进行一次更新才能再次使用预览。

4. Setting Up CardListView

您还需要更改包装清单视图,以便它与card view model一起使用。

打开CardListView.swift并将cards array属性替换为:

@ObservedObject var cardListViewModel = CardListViewModel()

有了这一更改,CardListView现在期望使用CardListViewModel而不是Cards数组。 @ObservedObject将订阅该属性,以便它可以侦听视图模型中的更改。

在主体中查找ForEach语句,并将其更改为如下所示:

ForEach(cardListViewModel.cardViewModels) { cardViewModel in
  CardView(cardViewModel: cardViewModel)
    .padding([.leading, .trailing])
}

现在,您将遍历cardListViewModel的各个卡片视图模型,并为每个模型创建一个CardView

由于CardListView现在需要CardListViewModel而不是Cards数组,因此将CardListView_Previews更改为:

CardListView(cardListViewModel: CardListViewModel())

构建并运行

根据需要添加任意数量的卡片,并查看它们如何立即显示在主屏幕上。


Updating Cards

该应用程序可让用户在获得正确答案时进行标记。 如果不是这种情况,则会弹出一条消息,告诉他们上次尝试失败。

打开Card.swift并修改id,如下所示:

@DocumentID var id: String?

注意:这样做会更改您的数据模型,因为id不会包含在其中。 下次您运行该应用程序时,以前的模型将不会显示。

在顶部添加此导入语句:

import FirebaseFirestoreSwift

使用此代码,您可以确保当FirebaseSDK将文档转换为Card时,Cloud Firestore中使用的Document Id会映射到id。 要对单个文档执行操作,您需要使用其document id对其进行引用。

打开CardRepository.swift并将下一个方法添加到CardRepository

func update(_ card: Card) {
  // 1
  guard let cardId = card.id else { return }

  // 2
  do {
    // 3
    try store.collection(path).document(cardId).setData(from: card)
  } catch {
    fatalError("Unable to update card: \(error.localizedDescription).")
  }
}

这段代码:

  • 1) 检查card.id是否具有值。
  • 2) 捕获代码生成的任何异常。 如果在更新文档时出现问题,则该应用程序将终止并显示致命错误。
  • 3) 使用pathcardId,它获取对cards集合中文档的引用,然后通过将card传递给setData(from:encoder:completion :)来更新字段。

现在,您需要更新视图模型。 打开CardViewModel.swift并将以下方法添加到CardViewModel

func update(card: Card) {
  cardRepository.update(card)
}

打开CardView.swift。 在frontView中的第二个Spacer()之后添加以下代码:

if !cardViewModel.card.successful {
  Text("You answered this one incorrectly before")
    .foregroundColor(.white)
    .font(.system(size: 11.0))
    .fontWeight(.bold)
    .padding()
}    

如果card的属性successful等于false,则此代码显示一条消息。

在继续之前,将以下三种方法添加到CardView

// 1
private func markCardAsUnsuccesful() {
  var updatedCard = cardViewModel.card
  updatedCard.successful = false
  update(card: updatedCard)
}

// 2
private func markCardAsSuccesful() {
  var updatedCard = cardViewModel.card
  updatedCard.successful = true
  update(card: updatedCard)
}

// 3
func update(card: Card) {
  cardViewModel.update(card: card)
  showContent.toggle()
}

该代码提供了两种方法来处理成功和失败的case,以及一种用于更新card的方法。

每种方法的作用如下:

  • 1) 将cardViewModel.card复制到updatedCard并将successful设置为false。 然后调用update(card :)
  • 2) 将cardViewModel.card复制到UpdatedCard并将successful设置为true。 然后调用update(card :)
  • 3) 将更新的卡片传递给update(card :),以便视图模型可以更新模型。 然后在showContent上调用toggle()来触发翻转动画。

接下来,将backView替换为以下内容:

var backView: some View {
  VStack {
    // 1
    Spacer()
    Text(cardViewModel.card.answer)
      .foregroundColor(.white)
      .font(.body)
      .padding(20.0)
      .multilineTextAlignment(.center)
      .animation(.easeInOut)
    Spacer()
    // 2
    HStack(spacing: 40) {
      Button(action: markCardAsSuccesful) {
        Image(systemName: "hand.thumbsup.fill")
          .padding()
          .background(Color.green)
          .font(.title)
          .foregroundColor(.white)
          .clipShape(Circle())
      }
      Button(action: markCardAsUnsuccesful) {
        Image(systemName: "hand.thumbsdown.fill")
          .padding()
          .background(Color.blue)
          .font(.title)
          .foregroundColor(.white)
          .clipShape(Circle())
      }
    }
    .padding()
  }
  .rotation3DEffect(.degrees(180), axis: (x: 0.0, y: 1.0, z: 0.0))
}

在这里,您添加了两个新按钮,以便用户可以指示他们是否正确回答了问题。

构建并运行。

点击任意卡,然后点击拇指向下thumb-down图标。 在底部,前视图显示一条消息,提示You answered this one incorrectly before


Removing Cards

用户应能够在需要时取出卡。

打开CardRepository.swift并在CardRepository的底部定义remove(_ :),如下所示:

func remove(_ card: Card) {
  // 1
  guard let cardId = card.id else { return }

  // 2
  store.collection(path).document(cardId).delete { error in
    if let error = error {
      print("Unable to remove card: \(error.localizedDescription)")
    }
  }
}

这段代码:

  • 1) 检查card.id是否具有值并将其存储在cardId中。
  • 2) 使用pathcardId获取对Cards集合中文档的引用,然后调用delete。 这将从Cloud Firestore中的集合中删除文档。

delete(completion :)还提供了一个闭包,您可以在其中处理任何错误。 闭包中的代码检查是否有错误,并将其打印到控制台。

打开CardViewModel.swift并向其中添加此方法,以便您的视图模型可以调用CardRepository上的remove(_ :),并传递实际的Card

func remove() {
  cardRepository.remove(card)
}

最后,打开CardView.swift并在Alert内部的primaryButton的尾随闭包中添加cardViewModel.remove(),因此如下所示:

Alert(
  title: Text("Remove Card"),
  message: Text("Are you sure you want to remove this card?"),
  primaryButton: .destructive(Text("Remove")) {
    cardViewModel.remove()
  },
  secondaryButton: .cancel())

这将在cardViewModel上调用remove()。 然后,视图模型执行逻辑以从数据库中删除卡。

构建并运行。

将您的任何卡片拖到顶部。 出现alert,要求您确认操作。 点击Remove,您的卡将消失。


Securing the Data

安全性对于任何应用程序都是必不可少的。 Firebase提供了一组身份验证方法,可用于让用户对您的应用程序进行身份验证。 对于本项目,您将实现匿名身份验证Anonymous Authentication

匿名身份验证(Anonymous Authentication)是一种身份验证类型,可让您为尚未注册应用的用户创建临时帐户,从而为他们提供了一层安全保护。 与安全规则(Security Rules)结合使用,匿名身份验证为此应用程序提供了足够的安全性。

要激活此身份验证模式,请转到Firebase控制台,在左侧边栏中选择Authentication,然后在顶部导航栏上选择Sign-in method。 转到Providers List的底部,选择Anonymous,然后单击右侧的开关将其启用。 最后,单击Save

注意:如果您没有看到顶部的导航栏,请单击Get Started以跳过介绍性屏幕。

现在,您需要创建一个身份验证服务。

1. Creating an authentication service

在项目导航器中,在Services下创建一个新的Swift文件,并将其命名为AuthenticationService.swift

将以下代码添加到新文件中:

import Firebase

// 1
class AuthenticationService: ObservableObject {
  // 2
  @Published var user: User?
  private var authenticationStateHandler: AuthStateDidChangeListenerHandle?

  // 3
  init() {
    addListeners()
  }

  // 4
  static func signIn() {
    if Auth.auth().currentUser == nil {
      Auth.auth().signInAnonymously()
    }
  }

  private func addListeners() {
    // 5
    if let handle = authenticationStateHandler {
      Auth.auth().removeStateDidChangeListener(handle)
    }

    // 6
    authenticationStateHandler = Auth.auth()
      .addStateDidChangeListener { _, user in
        self.user = user
      }
  }
}

这段代码:

  • 1) 声明AuthenticationService并将其符合ObservableObject
  • 2) 定义在身份验证过程发生时将包含User对象的用户。它还定义了一个authenticationStateHandler属性,以捕获用户对象中的更改,例如,当用户登录或注销时。
  • 3) 实现init()并调用addListeners(),以便在实例化该类时调用它。
  • 4) 添加signIn(),用于登录FirebaseAuthFirebase用户对象存储在currentUser中。

通过检查它是否为nil,可以避免不必要的调用。此值存储在本地,因此在第一次使用后,该应用使用同一用户。

  • 5) 检查是否已实例化处理程序,如果已实例化,则将其删除。
  • 6) 将addStateDidChangeListener(_ :)监听器分配给authenticationStateHandler

好的,您已经设置了身份验证服务(Authentication Service)

2. Using the authentication service

打开AppDelegate.swift并在application(_:didFinishLaunchingWithOptions:)FirebaseApp.configure()之后添加以下行:

AuthenticationService.signIn()

此代码可确保用户在应用启动时登录。

接下来,打开CardRepository.swift并将这些属性添加到类的顶部:

// 1
var userId = ""
// 2
private let authenticationService = AuthenticationService()
// 3
private var cancellables: Set<AnyCancellable> = []

这段代码:

  • 1) 声明userId,您将使用该ID存储Firebase生成的当前用户ID。
  • 2) 创建AuthenticationService的实例。
  • 3) 创建一组AnyCancellables。 此属性存储您的订阅,因此您以后可以取消订阅。

接下来,将init()更改为此:

init() {
  // 1
  authenticationService.$user
    .compactMap { user in
      user?.uid
    }
    .assign(to: \.userId, on: self)
    .store(in: &cancellables)

  // 2
  authenticationService.$user
    .receive(on: DispatchQueue.main)
    .sink { [weak self] _ in
      // 3
      self?.get()
    }
    .store(in: &cancellables)
}

在这里:

  • 1) 将用户ID从AuthenticationService绑定到存储库的userId。 它还将对象存储在cancellables中,以便以后可以取消。
  • 2) 该代码观察用户user的变化,使用receive(on:options :)设置代码执行的线程,然后使用sink(receiveValue:)附加订阅者。 这样可以保证,当您从AuthenticationService获取用户user时,闭包中的代码将在主线程中执行。
  • 3) 像在原始的初始化程序中一样,调用get()

add(_ :)更改为此:

func add(_ card: Card) {
  do {
    var newCard = card
    newCard.userId = userId
    _ = try store.collection(path).addDocument(from: newCard)
  } catch {
    fatalError("Unable to add card: \(error.localizedDescription).")
  }
}

在这里,您制作了card的副本,并将其userId更改为存储库的userId的值。 现在,每次创建新卡时,它都会包含Firebase生成的实际用户id

最后,在get().addSnapshotListener(_ :)之前添加以下行:

.whereField("userId", isEqualTo: userId)

此代码使您可以按userId过滤卡片。

3. Adding Authorization Using Security Rules

在网络浏览器中打开Firebase项目。 然后转到Cloud Firestore,然后单击顶部水平导航栏上的Rules。 您会看到类似以下内容:

此类似于JavaScript的代码是Firestore Security Rules。 这些规则定义用户是否有权访问或修改文档。 用以下代码替换现有代码:

// 1
rules_version = '2';
// 2
service cloud.firestore {
  // 3 
  match /databases/{database}/documents {
    // 4
    match /{document=**} {
      allow read, write: if request.auth != null;
    }
  }
}

这是做什么的:

  • 1) 将rules_version设置为“ 2”。 目前,这是最新版本,并确定如何解释以下代码。
  • 2) 指示这些规则适用于哪些服务。 在这种情况下,请使用Cloud Firestore
  • 3) 指定规则应匹配项目中的任何Cloud Firestore数据库。
  • 4) 指定仅经过身份验证的用户可以读取或写入文档。

这些规则确定了应用程序的授权部分。 通过身份验证,您可以了解用户是谁。 通过授权,您可以确定该用户可以做什么。

单击Publish以保存更改。 然后回到项目并构建并运行。

该应用目前已按用户userId进行过滤,因此不会显示任何卡片。如果您添加一个新的,它将出现。您甚至可以关闭并重新打开该应用程序,仅显示从现在开始创建的卡片。


Understanding Firestore Pricing

了解Cloud Firestore的定价可以节省您花费过多的金钱。请记住以下几点:

  • Cloud Firestore向您收取您执行的操作数:读取,写入和删除。
  • 价格从一个地点到另一个地点有所不同。
  • 您还必须支付数据库使用的存储空间和网络带宽。
  • 如果更改单个字段或完整的文档,则视为一次操作。
  • 读取次数是返回的记录数。因此,如果您的查询返回了十个文档,那么您将有十次读取。如果可能,请使用limit限制查询可以返回的文档数。
  • 您可以使用Alerts监控当前预算。您可以使用Google Cloud Console对其进行配置,这也可以让您检查以前的发票并设置所需的每日支出。
  • Google Cloud Operation可让您监控效果并获取指标,这些指标也可以帮助您制定预算。

如果可能,您还应该在本地缓存数据,以避免从Firestore请求数据。

您可以在the Firestore documentation文档中找到更多信息。

在本教程中,您学习了如何使用Cloud Firestore持久存储数据以及如何使用MVVM将其与SwiftUI视图集成。 您还从头开始学习了如何使用Firebase实施Anonymous Authentication

FirebaseCloud Firestore提供了更多功能。 如果您想更深入地了解Cloud Firestore,请查看官方的official Cloud Firestore documentation。 或查看Firebase Tutorial: Getting StartedFirebase Tutorial: Real-time ChatVideo Tutorial: Beginning Firebase

后记

本篇主要讲述了基于Firebase Cloud FirestoreSwiftUI iOS程序的持久性添加,感兴趣的给个赞或者关注~~~

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

推荐阅读更多精彩内容