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

版本記錄

版本號 時間
V1.0 2020.12.17 星期四

前言

今天翻閱蘋果的API文檔,發(fā)現(xiàn)多了一個框架SwiftUI粱栖,這里我們就一起來看一下這個框架。感興趣的看下面幾篇文章。
1. SwiftUI框架詳細(xì)解析 (一) —— 基本概覽(一)
2. SwiftUI框架詳細(xì)解析 (二) —— 基于SwiftUI的閃屏頁的創(chuàng)建(一)
3. SwiftUI框架詳細(xì)解析 (三) —— 基于SwiftUI的閃屏頁的創(chuàng)建(二)
4. SwiftUI框架詳細(xì)解析 (四) —— 使用SwiftUI進行蘋果登錄(一)
5. SwiftUI框架詳細(xì)解析 (五) —— 使用SwiftUI進行蘋果登錄(二)
6. SwiftUI框架詳細(xì)解析 (六) —— 基于SwiftUI的導(dǎo)航的實現(xiàn)(一)
7. SwiftUI框架詳細(xì)解析 (七) —— 基于SwiftUI的導(dǎo)航的實現(xiàn)(二)
8. SwiftUI框架詳細(xì)解析 (八) —— 基于SwiftUI的動畫的實現(xiàn)(一)
9. SwiftUI框架詳細(xì)解析 (九) —— 基于SwiftUI的動畫的實現(xiàn)(二)
10. SwiftUI框架詳細(xì)解析 (十) —— 基于SwiftUI構(gòu)建各種自定義圖表(一)
11. SwiftUI框架詳細(xì)解析 (十一) —— 基于SwiftUI構(gòu)建各種自定義圖表(二)
12. SwiftUI框架詳細(xì)解析 (十二) —— 基于SwiftUI創(chuàng)建Mind-Map UI(一)
13. SwiftUI框架詳細(xì)解析 (十三) —— 基于SwiftUI創(chuàng)建Mind-Map UI(二)

開始

首先看下主要內(nèi)容:

在本教程中,您將學(xué)習(xí)如何使用Firebase Cloud Firestore將持久性添加到SwiftUI iOS應(yīng)用程序篱蝇。內(nèi)容來自翻譯

接著看下主要內(nèi)容:

Swift 5, iOS 14, Xcode 12

接著就是主要內(nèi)容:

Google的移動后端服務(wù)徽曲,即Firebase零截,為應(yīng)用程序開發(fā)人員提供了從分析到分發(fā)(Analytics to Distribution),再到數(shù)據(jù)庫和身份驗證(Databases and Authentication)的所有功能秃臣。在本教程中涧衙,您將了解Cloud Firestore(該服務(wù)套件的一部分)以及如何將其與SwiftUI結(jié)合使用。

Cloud Firestore是一個靈活的NoSQL云數(shù)據(jù)庫,開發(fā)人員可以使用它實時存儲和同步應(yīng)用程序數(shù)據(jù)弧哎。您將使用它為FireCard提供數(shù)據(jù)和服務(wù)雁比,該應(yīng)用程序可通過創(chuàng)建卡片來幫助用戶記住概念。

在此過程中傻铣,您將學(xué)習(xí)如何:

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

打開入門項目章贞。

Firecards是一個簡單的工具,可讓用戶通過提供問題和答案來創(chuàng)建卡片非洲。稍后,他們可以通過閱讀問題并輕按卡片上的內(nèi)容來測試自己的記憶力蜕径,以查看其答案是否正確两踏。

目前,該應(yīng)用沒有持久性存儲(persistence)兜喻,因此用戶無法對其做太多事情梦染。但是您可以通過將Firestore添加到此SwiftUI應(yīng)用中來解決此問題。

您將不會使用它幾個步驟朴皆,而是在Xcode中打開FireCards.xcodeproj帕识。

注意:該項目使用Swift Package Manager來管理依賴項。由于Firebase SDK很大遂铡,因此建議您在閱讀本教程的同時在后臺打開項目肮疗,并讓其獲取并解決所有依賴項。

1. Setting Up Firebase

您必須先創(chuàng)建一個Firebase帳戶扒接,然后才能使用Cloud Firestore伪货。轉(zhuǎn)到Firebase網(wǎng)站。在右上角钾怔,單擊Go to console碱呼。然后提供您的Google帳戶憑據(jù)(Google account credentials);如果您還沒有憑據(jù)宗侦,請創(chuàng)建一個愚臀。

接下來,單擊+ Add project矾利。將會出現(xiàn)一個modal姑裂,詢問您的項目名稱。類型FireCard

下一步要求您為項目啟用Google Analytics(分析)梦皮。 由于本教程沒有介紹Analytics(分析)炭分,因此請單擊底部的切換按鈕將其禁用。 然后單擊Create project

幾秒鐘后剑肯,您會看到一條消息捧毛,上面寫著Your new project is ready。 點擊Continue,您將看到項目的儀表板:

您可以在此處訪問所有Firebase服務(wù)呀忧。 選擇Add an app to get started的圓圈iOS按鈕開始使用师痕。 在iOS Bundle ID字段中輸入com.raywenderlich.firecards,然后單擊Register app

按照概述的說明下載GoogleService-Info.plist并將其拖到FireCards Xcode項目中:

當(dāng)Xcode提示時而账,請確保選中Copy Items if needed胰坟。

下一步要求您將Firebase SDK添加到您的iOS應(yīng)用中,這已經(jīng)為您完成泞辐。 單擊Next轉(zhuǎn)到Add initialization code步驟笔横。

打開AppDelegate.swift。 通過添加以下導(dǎo)入語句咐吼,確保包括Firebase

import Firebase

接下來吹缔,將此代碼添加到application(_:didFinishLaunchingWithOptions :)中的return語句之前:

FirebaseApp.configure()

Firebase項目的網(wǎng)頁上,單擊Next锯茄,然后單擊Continue to console

這會將您帶回到項目的概述頁面:

您完成了Firebase的設(shè)置厢塘,并為您的應(yīng)用授予了訪問所有Firebase服務(wù)的權(quán)限。 接下來肌幽,您將配置Cloud Firestore晚碾。

2. Setting Up Cloud Firestore

在左側(cè)菜單的Develop下,單擊Cloud Firestore喂急。 然后格嘁,單擊Create database

將出現(xiàn)一個modal,向您顯示下一步:

  • Cloud Firestore設(shè)置安全規(guī)則煮岁。
  • 設(shè)置Cloud Firestore位置讥蔽。

Firebase使用安全規(guī)則來處理數(shù)據(jù)訪問授權(quán)。 選擇Start in test mode画机,這將使您的數(shù)據(jù)在未經(jīng)授權(quán)的情況下可訪問30天冶伞。

盡管這對于測試項目是可以的,但是您應(yīng)該始終設(shè)置適當(dāng)?shù)?code>Security Rules步氏。 幸運的是响禽,您稍后將在Adding Authorization Using Security Rules中進行介紹。

點擊Next荚醒。 現(xiàn)在芋类,助手將詢問您要將數(shù)據(jù)存儲在何處。

請注意界阁,該位置可能會影響您的帳單侯繁,以后將無法更改。 現(xiàn)在泡躯,選擇nam5(us-central)贮竟,然后單擊Enable丽焊。

現(xiàn)在,Firebase將為您配置所有內(nèi)容咕别。 然后技健,它將把您轉(zhuǎn)到Firebase項目的Cloud Firestore部分:

在這里,您將了解如何實時插入惰拱,刪除或更新數(shù)據(jù)雌贱。 您也可以根據(jù)需要手動操作它。


Architecting the App Using MVVM

對于此項目偿短,您將使用Model-View-View ModelMVVM來構(gòu)建應(yīng)用程序的組件欣孤。

MVVM是一種結(jié)構(gòu)設(shè)計模式,它將構(gòu)成應(yīng)用程序的元素分為Views, View Models and Models翔冀。 這種設(shè)計模式有助于開發(fā)人員從視圖中分離業(yè)務(wù)邏輯导街,并保持必要的關(guān)注點分離,以使視圖和模型與數(shù)據(jù)源和業(yè)務(wù)邏輯不可知纤子。

由于您將Cloud Firestore用于數(shù)據(jù)持久性,因此將添加一個層來處理與數(shù)據(jù)源進行交互所需的邏輯款票。 對于此項目控硼,您將使用存儲庫模式。 下圖顯示了應(yīng)用程序體系結(jié)構(gòu)的最終表示形式:

  • Models保存應(yīng)用程序數(shù)據(jù)艾少。它們代表了您的應(yīng)用需要管理的實體卡乾。
  • Views構(gòu)成構(gòu)成應(yīng)用程序的視覺元素,并負(fù)責(zé)顯示模型中數(shù)據(jù)缚够。
  • View Model通過轉(zhuǎn)換模型中的數(shù)據(jù)使其可以顯示在視圖中幔妨,從而使模型與視圖之間的關(guān)系成為可能。
  • Repository表示處理數(shù)據(jù)源data source通信的抽象谍椅。在這種情況下误堡,數(shù)據(jù)源是Cloud Firestore雏吭。當(dāng)View Model需要對數(shù)據(jù)進行任何操作時锁施,它會與Repository進行通信,并通知有關(guān)數(shù)據(jù)更改的視圖杖们。

1. Thinking in Collections and Documents

Cloud FirestoreNoSQL數(shù)據(jù)庫悉抵。它使用集合和文檔(collections and document)來構(gòu)造數(shù)據(jù)。

集合保存文檔摘完。這些文檔documents的字段構(gòu)成了您應(yīng)用程序的實體姥饰,在本例中為卡片。因此孝治,卡片是文檔列粪,卡片組是集合审磁。

這是應(yīng)用程序數(shù)據(jù)結(jié)構(gòu)的直觀表示:

您可以編寫查詢(queries)以從集合中獲取數(shù)據(jù),或插入篱竭,更新或刪除文檔力图。 為此,您需要使用唯一標(biāo)識符創(chuàng)建對集合或特定文檔的引用掺逼。 創(chuàng)建新文檔時吃媒,您可以手動傳遞此標(biāo)識符,否則Cloud Firestore會為您創(chuàng)建一個吕喘。

聊夠了赘那,該寫代碼了!

2. Adding New Cards

首先創(chuàng)建Repository以訪問數(shù)據(jù)氯质。

在項目導(dǎo)航器中募舟,右鍵單擊Repositories,然后單擊New file…闻察。 創(chuàng)建一個名為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) 導(dǎo)入FirebaseFirestore,F(xiàn)irebaseFirestoreSwiftCombine辕漂。 FirebaseFirestore使您可以訪問Firestore API呢灶,而Combine為Swift提供了一組聲明性API。

FirebaseFirestoreSwift添加了一些很酷的功能來幫助您將Firestore與模型集成钉嘹。它使您可以將Cards轉(zhuǎn)換為文檔鸯乃,并將文檔轉(zhuǎn)換為Cards

  • 2) 定義CardRepository并使遵循ObservableObject跋涣。 ObservableObject使用發(fā)布者幫助此類發(fā)出更改缨睡,因此其他對象可以偵聽并做出相應(yīng)的反應(yīng)。
  • 3) 然后陈辱,聲明path并分配cards的值奖年。這是Firestore中的集合名稱。
  • 4) 聲明store并分配對Firestore實例的引用性置。
  • 5) 接下來拾并,定義add(_ :)并使用do-catch塊捕獲由代碼引發(fā)的任何錯誤。如果在更新文檔時出了點問題鹏浅,則會因致命錯誤終止應(yīng)用的執(zhí)行嗅义。
  • 6) 使用path創(chuàng)建對cards集合的引用,然后將card傳遞到addDocument(from:encoder:completion :)隐砸。這會將新卡添加到集合中之碗。

使用上面的代碼,編譯器將報addDocument(from:encoder:completion :)要求Card符合Encodable季希。要解決此問題褪那,請打開Card.swift并將類定義更改為此:

struct Card: Identifiable, Codable {

通過添加Codable幽纷,Swift可以無縫地對Cards進行序列化和反序列化。 其中包括導(dǎo)致錯誤的Encodable和將Firestore文檔轉(zhuǎn)換為Swift對象時將使用的Decodable博敬。

3. Adding the View Model

您需要一個視圖模型才能將模型與視圖連接友浸。 在Project導(dǎo)航器的ViewModels組下,創(chuàng)建一個名為CardListViewModel.swift的新Swift文件偏窝。

將此添加到新文件:

// 1
import Combine

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

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

下面進行細(xì)分:

  • 1) Combine為您提供了處理異步代碼的API收恢。
  • 2) 您聲明CardListViewModel并使它符合ObservableObject。 這使您可以偵聽此類型對象發(fā)出的更改祭往。
  • 3) @Published為此屬性創(chuàng)建一個發(fā)布者伦意,以便您可以訂閱它。
  • 4) 您將card傳遞到存儲庫硼补,以便可以將其添加到集合中驮肉。

打開NewCardForm.swift并為您創(chuàng)建的view model添加一個屬性,緊隨NewCardForm中的其他屬性之后:

@ObservedObject var cardListViewModel: CardListViewModel

之前的更改將使Xcode Preview停止工作已骇,因為現(xiàn)在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) 使用已經(jīng)在頂部聲明的questionanswer屬性創(chuàng)建Card褪储。
  • 2) 使用view model添加新card奈辰。
  • 3) 關(guān)閉當(dāng)前視圖。

然后乱豆,通過將Button(action:{}){替換為下面代碼,將此新方法稱為Add New Card的操作吊趾。

Button(action: addCard) {

最后宛裕,打開CardListView.swift,找到.sheet修飾符论泛,并通過現(xiàn)在傳遞一個新的視圖模型實例來修復(fù)編譯器錯誤揩尸。 您稍后將使用共享實例。

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

構(gòu)建并運行屁奏。

點擊右上角的+岩榆。 填寫question and answer字段,然后點擊Add New Card坟瓢。

嗯勇边,什么都沒發(fā)生≌哿卡片未顯示在主屏幕中:

在網(wǎng)絡(luò)瀏覽器中打開Firebase Console粒褒,然后轉(zhuǎn)到Cloud Firestore部分。 Firestore自動創(chuàng)建了cards收藏诚镰。 單擊標(biāo)識符以導(dǎo)航到新文檔:

您的數(shù)據(jù)存儲在Firebase中奕坟,但您仍未實現(xiàn)檢索和顯示卡片的邏輯祥款。


Retrieving and Displaying Cards

現(xiàn)在該展示您的卡片了! 首先月杉,您需要創(chuàng)建一個view model來代表一張Card刃跛。

在項目導(dǎo)航器的ViewModels下柱告,創(chuàng)建一個名為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兼容,以便它可以發(fā)出更改和Identifiable护戳,從而保證您可以迭代CardViewModels的數(shù)組首懈。
  • 2) 這為實際的card模型提供了引用绊率。 @Published為此屬性創(chuàng)建一個發(fā)布者,以便您可以訂閱它究履。
  • 3) cancellables用于存儲您的訂閱滤否,因此您以后可以取消訂閱。
  • 4) id是要求遵循Identifiable的屬性最仑。 它應(yīng)該是唯一的標(biāo)識符藐俺。
  • 5) 在卡的ID和視圖模型的ID之間為card設(shè)置綁定。 然后將對象存儲在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為此屬性創(chuàng)建一個發(fā)布者,以便您可以訂閱它剑逃。 每次修改此數(shù)組時浙宜,所有偵聽者都會做出相應(yīng)的反應(yīng)。
  • 2) 創(chuàng)建初始化方法并調(diào)用get()蛹磺。
  • 3) 使用path獲取對集合根目錄的引用粟瞬,并添加一個偵聽器以接收集合中的更改。
  • 4) 檢查是否發(fā)生錯誤萤捆,打印錯誤消息并返回裙品。
  • 5) 在querySnapshot.documents上使用compactMap(_ :)遍歷所有元素。 如果querySnapshotnil俗或,則改為設(shè)置一個空數(shù)組市怎。
  • 6) 使用data(as:decoder :)將每個文檔映射為Card。 您可以這樣做蕴侣,這要歸功于您在頂部導(dǎo)入的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數(shù)組揩懒。
  • 2) 創(chuàng)建一組AnyCancellables什乙。 它將用于存儲您的訂閱,以便您以后可以取消訂閱已球。

仍在視圖模型中時臣镣,添加以下初始化程序:

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

您添加的代碼:

  • 1) 監(jiān)聽cards,并將數(shù)組的每個Card元素映射到CardViewModel中智亮。 這將創(chuàng)建一個CardViewModels數(shù)組忆某。
  • 2) 將前一個映射操作的結(jié)果分配給cardViewModels
  • 3) 將此預(yù)訂的實例存儲在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))
  }
}

進行了這些更改后毕莱,您現(xiàn)在可以直接傳遞預(yù)期的CardViewModel而不是Card模型器贩。 但是,您需要再進行一次更新才能再次使用預(yù)覽朋截。

4. Setting Up CardListView

您還需要更改包裝清單視圖磨澡,以便它與card view model一起使用。

打開CardListView.swift并將cards array屬性替換為:

@ObservedObject var cardListViewModel = CardListViewModel()

有了這一更改质和,CardListView現(xiàn)在期望使用CardListViewModel而不是Cards數(shù)組。 @ObservedObject將訂閱該屬性稚字,以便它可以偵聽視圖模型中的更改饲宿。

在主體中查找ForEach語句,并將其更改為如下所示:

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

現(xiàn)在胆描,您將遍歷cardListViewModel的各個卡片視圖模型瘫想,并為每個模型創(chuàng)建一個CardView

由于CardListView現(xiàn)在需要CardListViewModel而不是Cards數(shù)組昌讲,因此將CardListView_Previews更改為:

CardListView(cardListViewModel: CardListViewModel())

構(gòu)建并運行

根據(jù)需要添加任意數(shù)量的卡片国夜,并查看它們?nèi)绾瘟⒓达@示在主屏幕上。


Updating Cards

該應(yīng)用程序可讓用戶在獲得正確答案時進行標(biāo)記短绸。 如果不是這種情況车吹,則會彈出一條消息筹裕,告訴他們上次嘗試失敗。

打開Card.swift并修改id窄驹,如下所示:

@DocumentID var id: String?

注意:這樣做會更改您的數(shù)據(jù)模型朝卒,因為id不會包含在其中。 下次您運行該應(yīng)用程序時乐埠,以前的模型將不會顯示抗斤。

在頂部添加此導(dǎo)入語句:

import FirebaseFirestoreSwift

使用此代碼,您可以確保當(dāng)FirebaseSDK將文檔轉(zhuǎn)換為Card時丈咐,Cloud Firestore中使用的Document Id會映射到id瑞眼。 要對單個文檔執(zhí)行操作,您需要使用其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) 捕獲代碼生成的任何異常。 如果在更新文檔時出現(xiàn)問題歹河,則該應(yīng)用程序?qū)⒔K止并顯示致命錯誤掩浙。
  • 3) 使用pathcardId,它獲取對cards集合中文檔的引用秸歧,然后通過將card傳遞給setData(from:encoder:completion :)來更新字段厨姚。

現(xiàn)在,您需要更新視圖模型键菱。 打開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,則此代碼顯示一條消息经备。

在繼續(xù)之前拭抬,將以下三種方法添加到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復(fù)制到updatedCard并將successful設(shè)置為false造虎。 然后調(diào)用update(card :)
  • 2) 將cardViewModel.card復(fù)制到UpdatedCard并將successful設(shè)置為true纷闺。 然后調(diào)用update(card :)算凿。
  • 3) 將更新的卡片傳遞給update(card :),以便視圖模型可以更新模型犁功。 然后在showContent上調(diào)用toggle()來觸發(fā)翻轉(zhuǎn)動畫氓轰。

接下來,將backView替換為以下內(nèi)容:

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))
}

在這里浸卦,您添加了兩個新按鈕署鸡,以便用戶可以指示他們是否正確回答了問題。

構(gòu)建并運行。

點擊任意卡靴庆,然后點擊拇指向下thumb-down圖標(biāo)时捌。 在底部,前視圖顯示一條消息撒穷,提示You answered this one incorrectly before


Removing Cards

用戶應(yīng)能夠在需要時取出卡匣椰。

打開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集合中文檔的引用禽笑,然后調(diào)用delete。 這將從Cloud Firestore中的集合中刪除文檔蛤奥。

delete(completion :)還提供了一個閉包佳镜,您可以在其中處理任何錯誤。 閉包中的代碼檢查是否有錯誤凡桥,并將其打印到控制臺蟀伸。

打開CardViewModel.swift并向其中添加此方法,以便您的視圖模型可以調(diào)用CardRepository上的remove(_ :)缅刽,并傳遞實際的Card

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

最后啊掏,打開CardView.swift并在Alert內(nèi)部的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上調(diào)用remove()衰猛。 然后迟蜜,視圖模型執(zhí)行邏輯以從數(shù)據(jù)庫中刪除卡。

構(gòu)建并運行啡省。

將您的任何卡片拖到頂部娜睛。 出現(xiàn)alert,要求您確認(rèn)操作卦睹。 點擊Remove畦戒,您的卡將消失。


Securing the Data

安全性對于任何應(yīng)用程序都是必不可少的结序。 Firebase提供了一組身份驗證方法障斋,可用于讓用戶對您的應(yīng)用程序進行身份驗證。 對于本項目徐鹤,您將實現(xiàn)匿名身份驗證Anonymous Authentication配喳。

匿名身份驗證(Anonymous Authentication)是一種身份驗證類型,可讓您為尚未注冊應(yīng)用的用戶創(chuàng)建臨時帳戶凳干,從而為他們提供了一層安全保護。 與安全規(guī)則(Security Rules)結(jié)合使用被济,匿名身份驗證為此應(yīng)用程序提供了足夠的安全性救赐。

要激活此身份驗證模式,請轉(zhuǎn)到Firebase控制臺,在左側(cè)邊欄中選擇Authentication经磅,然后在頂部導(dǎo)航欄上選擇Sign-in method泌绣。 轉(zhuǎn)到Providers List的底部,選擇Anonymous预厌,然后單擊右側(cè)的開關(guān)將其啟用阿迈。 最后,單擊Save轧叽。

注意:如果您沒有看到頂部的導(dǎo)航欄苗沧,請單擊Get Started以跳過介紹性屏幕。

現(xiàn)在炭晒,您需要創(chuàng)建一個身份驗證服務(wù)待逞。

1. Creating an authentication service

在項目導(dǎo)航器中,在Services下創(chuàng)建一個新的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) 定義在身份驗證過程發(fā)生時將包含User對象的用戶震束。它還定義了一個authenticationStateHandler屬性怜庸,以捕獲用戶對象中的更改,例如垢村,當(dāng)用戶登錄或注銷時割疾。
  • 3) 實現(xiàn)init()并調(diào)用addListeners(),以便在實例化該類時調(diào)用它肝断。
  • 4) 添加signIn()杈曲,用于登錄FirebaseAuthFirebase用戶對象存儲在currentUser中胸懈。

通過檢查它是否為nil担扑,可以避免不必要的調(diào)用。此值存儲在本地趣钱,因此在第一次使用后涌献,該應(yīng)用使用同一用戶。

  • 5) 檢查是否已實例化處理程序首有,如果已實例化燕垃,則將其刪除。
  • 6) 將addStateDidChangeListener(_ :)監(jiān)聽器分配給authenticationStateHandler井联。

好的卜壕,您已經(jīng)設(shè)置了身份驗證服務(wù)(Authentication Service)

2. Using the authentication service

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

AuthenticationService.signIn()

此代碼可確保用戶在應(yīng)用啟動時登錄烙常。

接下來轴捎,打開CardRepository.swift并將這些屬性添加到類的頂部:

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

這段代碼:

  • 1) 聲明userId,您將使用該ID存儲Firebase生成的當(dāng)前用戶ID。
  • 2) 創(chuàng)建AuthenticationService的實例侦副。
  • 3) 創(chuàng)建一組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 :)設(shè)置代碼執(zhí)行的線程细燎,然后使用sink(receiveValue:)附加訂閱者两曼。 這樣可以保證,當(dāng)您從AuthenticationService獲取用戶user時玻驻,閉包中的代碼將在主線程中執(zhí)行悼凑。
  • 3) 像在原始的初始化程序中一樣,調(diào)用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的值嗤锉。 現(xiàn)在渔欢,每次創(chuàng)建新卡時,它都會包含Firebase生成的實際用戶id瘟忱。

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

.whereField("userId", isEqualTo: userId)

此代碼使您可以按userId過濾卡片。

3. Adding Authorization Using Security Rules

在網(wǎng)絡(luò)瀏覽器中打開Firebase項目访诱。 然后轉(zhuǎn)到Cloud Firestore垫挨,然后單擊頂部水平導(dǎo)航欄上的Rules。 您會看到類似以下內(nèi)容:

此類似于JavaScript的代碼是Firestore Security Rules触菜。 這些規(guī)則定義用戶是否有權(quán)訪問或修改文檔九榔。 用以下代碼替換現(xiàn)有代碼:

// 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設(shè)置為“ 2”。 目前涡相,這是最新版本哲泊,并確定如何解釋以下代碼。
  • 2) 指示這些規(guī)則適用于哪些服務(wù)催蝗。 在這種情況下切威,請使用Cloud Firestore
  • 3) 指定規(guī)則應(yīng)匹配項目中的任何Cloud Firestore數(shù)據(jù)庫丙号。
  • 4) 指定僅經(jīng)過身份驗證的用戶可以讀取或?qū)懭胛臋n先朦。

這些規(guī)則確定了應(yīng)用程序的授權(quán)部分且预。 通過身份驗證,您可以了解用戶是誰烙无。 通過授權(quán),您可以確定該用戶可以做什么遍尺。

單擊Publish以保存更改截酷。 然后回到項目并構(gòu)建并運行。

該應(yīng)用目前已按用戶userId進行過濾乾戏,因此不會顯示任何卡片迂苛。如果您添加一個新的,它將出現(xiàn)鼓择。您甚至可以關(guān)閉并重新打開該應(yīng)用程序三幻,僅顯示從現(xiàn)在開始創(chuàng)建的卡片。


Understanding Firestore Pricing

了解Cloud Firestore的定價可以節(jié)省您花費過多的金錢呐能。請記住以下幾點:

  • Cloud Firestore向您收取您執(zhí)行的操作數(shù):讀取念搬,寫入和刪除。
  • 價格從一個地點到另一個地點有所不同摆出。
  • 您還必須支付數(shù)據(jù)庫使用的存儲空間和網(wǎng)絡(luò)帶寬朗徊。
  • 如果更改單個字段或完整的文檔,則視為一次操作偎漫。
  • 讀取次數(shù)是返回的記錄數(shù)爷恳。因此,如果您的查詢返回了十個文檔象踊,那么您將有十次讀取温亲。如果可能,請使用limit限制查詢可以返回的文檔數(shù)杯矩。
  • 您可以使用Alerts監(jiān)控當(dāng)前預(yù)算栈虚。您可以使用Google Cloud Console對其進行配置,這也可以讓您檢查以前的發(fā)票并設(shè)置所需的每日支出菊碟。
  • Google Cloud Operation可讓您監(jiān)控效果并獲取指標(biāo)节芥,這些指標(biāo)也可以幫助您制定預(yù)算。

如果可能逆害,您還應(yīng)該在本地緩存數(shù)據(jù)头镊,以避免從Firestore請求數(shù)據(jù)。

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

在本教程中相艇,您學(xué)習(xí)了如何使用Cloud Firestore持久存儲數(shù)據(jù)以及如何使用MVVM將其與SwiftUI視圖集成。 您還從頭開始學(xué)習(xí)了如何使用Firebase實施Anonymous Authentication纯陨。

FirebaseCloud Firestore提供了更多功能坛芽。 如果您想更深入地了解Cloud Firestore留储,請查看官方的official Cloud Firestore documentation。 或查看Firebase Tutorial: Getting Started咙轩,Firebase Tutorial: Real-time Chat获讳,Video Tutorial: Beginning Firebase

后記

本篇主要講述了基于Firebase Cloud FirestoreSwiftUI iOS程序的持久性添加活喊,感興趣的給個贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末丐膝,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子钾菊,更是在濱河造成了極大的恐慌帅矗,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件煞烫,死亡現(xiàn)場離奇詭異浑此,居然都是意外死亡,警方通過查閱死者的電腦和手機滞详,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門凛俱,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人茵宪,你說我怎么就攤上這事最冰。” “怎么了稀火?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵暖哨,是天一觀的道長。 經(jīng)常有香客問我凰狞,道長篇裁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任赡若,我火速辦了婚禮达布,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘逾冬。我一直安慰自己黍聂,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布身腻。 她就那樣靜靜地躺著产还,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嘀趟。 梳的紋絲不亂的頭發(fā)上脐区,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機與錄音她按,去河邊找鬼牛隅。 笑死炕柔,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的媒佣。 我是一名探鬼主播匕累,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼默伍!你這毒婦竟也來了哩罪?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤巡验,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后碘耳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體显设,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年辛辨,在試婚紗的時候發(fā)現(xiàn)自己被綠了捕捂。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡斗搞,死狀恐怖指攒,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情僻焚,我是刑警寧澤允悦,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站虑啤,受9級特大地震影響隙弛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜狞山,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一全闷、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧萍启,春花似錦总珠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至屡律,卻和暖如春腌逢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背超埋。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工搏讶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留佳鳖,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓媒惕,卻偏偏與公主長得像系吩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子妒蔚,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

推薦閱讀更多精彩內(nèi)容