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進(jìn)行蘋果登錄(一)
5. SwiftUI框架詳細(xì)解析 (五) —— 使用SwiftUI進(jìn)行蘋果登錄(二)
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(二)
14. SwiftUI框架詳細(xì)解析 (十四) —— 基于Firebase Cloud Firestore的SwiftUI iOS程序的持久性添加(一)

源碼

1. Swift

1. AuthenticationService.swift
import Foundation
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
      }
  }
}
2. CardRepository.swift
import Foundation

// 1
import FirebaseFirestore
import FirebaseFirestoreSwift
import Combine

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

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

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

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

  func get() {
    // 3
    store.collection(path)
      .whereField("userId", isEqualTo: userId)
      .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)
        } ?? []
      }
  }

  // 4
  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).")
    }
  }

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

  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)")
      }
    }
  }
}
3. CardListViewModel.swift
import Foundation

// 1
import Combine

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

  // 3
  @Published var cardRepository = CardRepository()

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

  // 4
  func add(_ card: Card) {
    cardRepository.add(card)
  }
}
4. CardViewModel.swift
import Foundation
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
    // 6
    $card
      .compactMap { $0.id }
      .assign(to: \.id, on: self)
      .store(in: &cancellables)
  }

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

  func remove() {
    cardRepository.remove(card)
  }
}
5. CardView.swift
import SwiftUI

struct CardView: View {
  var cardViewModel: CardViewModel
  @State var showContent: Bool = false
  @State var viewState = CGSize.zero
  @State var showAlert = false

  var body: some View {
    ZStack(alignment: .center) {
      backView.opacity(showContent ? 1 : 0)
      frontView.opacity(showContent ? 0 : 1)
    }
    .frame(width: 250, height: 400)
    .background(Color.orange)
    .cornerRadius(20)
    .shadow(color: Color(.blue).opacity(0.3), radius: 5, x: 10, y: 10)
    .rotation3DEffect(.degrees(showContent ? 180.0 : 0.0), axis: (x: 0, y: -1, z: 0))
    .offset(x: viewState.width, y: viewState.height)
    .animation(.spring(response: 0.6, dampingFraction: 0.8, blendDuration: 0))
    .onTapGesture {
      withAnimation {
        showContent.toggle()
      }
    }
    .gesture(
      DragGesture()
        .onChanged { value in
          viewState = value.translation
        }
      .onEnded { value in
        if value.location.y < value.startLocation.y - 40.0 {
          showAlert.toggle()
        }
        viewState = .zero
      }
    )
      .alert(isPresented: $showAlert) {
        Alert(
          title: Text("Remove Card"),
          message: Text("Are you sure you want to remove this card?"),
          primaryButton: .destructive(Text("Remove")) {
            cardViewModel.remove()
          },
          secondaryButton: .cancel())
      }
  }

  var frontView: some View {
    VStack(alignment: .center) {
      Spacer()
      Text(cardViewModel.card.question)
        .foregroundColor(.white)
        .font(.system(size: 20))
        .fontWeight(.bold)
        .multilineTextAlignment(.center)
        .padding(20.0)
      Spacer()
      if !cardViewModel.card.successful {
        Text("You answered this one incorrectly before")
          .foregroundColor(.white)
          .font(.system(size: 11.0))
          .fontWeight(.bold)
          .padding()
      }
    }
  }

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

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

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

struct NewCardForm: View {
  @State var question: String = ""
  @State var answer: String = ""
  @Environment(\.presentationMode) var presentationMode
  @ObservedObject var cardListViewModel: CardListViewModel

  var body: some View {
    VStack(alignment: .center, spacing: 30) {
      VStack(alignment: .leading, spacing: 10) {
        Text("Question")
          .foregroundColor(.gray)
        TextField("Enter the question", text: $question)
          .textFieldStyle(RoundedBorderTextFieldStyle())
      }
      VStack(alignment: .leading, spacing: 10) {
        Text("Answer")
          .foregroundColor(.gray)
        TextField("Enter the answer", text: $answer)
          .textFieldStyle(RoundedBorderTextFieldStyle())
      }

      Button(action: addCard) {
        Text("Add New Card")
          .foregroundColor(.blue)
      }
      Spacer()
    }
    .padding(EdgeInsets(top: 80, leading: 40, bottom: 0, trailing: 40))
  }

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

struct NewCardForm_Previews: PreviewProvider {
  static var previews: some View {
    NewCardForm(cardListViewModel: CardListViewModel())
  }
}
7. CardListView.swift
import SwiftUI

struct CardListView: View {
  @ObservedObject var cardListViewModel = CardListViewModel()
  @State var showForm = false

  var body: some View {
    NavigationView {
      VStack {
        Spacer()
        VStack {
          GeometryReader { geometry in
            ScrollView(.horizontal) {
              HStack(spacing: 10) {
                ForEach(cardListViewModel.cardViewModels) { cardViewModel in
                  CardView(cardViewModel: cardViewModel)
                    .padding([.leading, .trailing])
                }
              }.frame(height: geometry.size.height)
            }
          }
        }
        Spacer()
      }
      .sheet(isPresented: $showForm) {
        NewCardForm(cardListViewModel: CardListViewModel())
      }
      .navigationBarTitle("?? Fire Cards")
        // swiftlint:disable multiple_closures_with_trailing_closure
        .navigationBarItems(trailing: Button(action: { showForm.toggle() }) {
          Image(systemName: "plus")
            .font(.title)
        })
    }
    .navigationViewStyle(StackNavigationViewStyle())
  }
}

struct CardListView_Previews: PreviewProvider {
  static var previews: some View {
    CardListView(cardListViewModel: CardListViewModel())
  }
}
8. Card.swift
import Foundation
import FirebaseFirestoreSwift

struct Card: Identifiable, Codable {
  @DocumentID var id: String?
  var question: String
  var answer: String
  var successful: Bool = true
  var userId: String?
}

#if DEBUG
let testData = (1...10).map { i in
  Card(question: "Question #\(i)", answer: "Answer #\(i)")
}
#endif
9. AppDelegate.swift
import UIKit
import Firebase

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
  // MARK: - UISceneSession Lifecycle

  func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
  }

  func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
    FirebaseApp.configure()
    AuthenticationService.signIn()
    return true
  }
10. SceneDelegate.swift
import UIKit
import SwiftUI

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  var window: UIWindow?

  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    if let windowScene = scene as? UIWindowScene {
      let window = UIWindow(windowScene: windowScene)
      window.rootViewController = UIHostingController(rootView: ContentView())
      self.window = window
      window.makeKeyAndVisible()
    }
  }
}
11. ContentView.swift
import SwiftUI

struct ContentView: View {
  var body: some View {
    CardListView()
  }
}

struct ContentView_Previews: PreviewProvider {
  static var previews: some View {
    ContentView()
  }
}

后記

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末胡诗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌肛著,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件跺讯,死亡現(xiàn)場離奇詭異枢贿,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)刀脏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門局荚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人愈污,你說我怎么就攤上這事耀态。” “怎么了暂雹?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵首装,是天一觀的道長。 經(jīng)常有香客問我杭跪,道長仙逻,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任涧尿,我火速辦了婚禮桨醋,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘现斋。我一直安慰自己喜最,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布庄蹋。 她就那樣靜靜地躺著瞬内,像睡著了一般迷雪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上虫蝶,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天章咧,我揣著相機(jī)與錄音,去河邊找鬼能真。 笑死赁严,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的粉铐。 我是一名探鬼主播疼约,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蝙泼!你這毒婦竟也來了程剥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤汤踏,失蹤者是張志新(化名)和其女友劉穎织鲸,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體溪胶,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡搂擦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了哗脖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瀑踢。...
    茶點(diǎn)故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖懒熙,靈堂內(nèi)的尸體忽然破棺而出丘损,到底是詐尸還是另有隱情普办,我是刑警寧澤工扎,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站衔蹲,受9級特大地震影響肢娘,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜舆驶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一橱健、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沙廉,春花似錦拘荡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽网缝。三九已至,卻和暖如春蟋定,著一層夾襖步出監(jiān)牢的瞬間粉臊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工驶兜, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留扼仲,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓抄淑,卻偏偏與公主長得像屠凶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子蝇狼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評論 2 348

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