Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Firebase & SwiftUI Workshop

Peter Friese
September 05, 2022

Firebase & SwiftUI Workshop

In this workshop, you will learn how to build a SwiftUI application with Firebase. We will cover the following topics:

- Data modeling for Firestore
- Efficiently mapping Firestore data using Swift’s Codable protocol
- Fetching data from Firestore using snapshot listeners
- Connecting SwiftUI’s state management system to Firestore to implement real-time sync
- Securing your user’s data using Firebase Security Rules
- Signing in your users using Firebase Authentication

We will be using the latest versions of Firebase and SwiftUI, making use of Combine and async/await to demonstrate how to call asynchronous APIs using modern Swift technologies. Please bring your laptop, making sure to install the latest stable version of Xcode before the workshop.

Peter Friese

September 05, 2022
Tweet

More Decks by Peter Friese

Other Decks in Programming

Transcript

  1. Peter Friese | Developer Advocate, Firebase Marina Coelho | Developer

    Relations Engineer, Firebase  + Swi!UI & Firebase Workshop @coelho_dev @pete!riese
  2. Make It So ✨ Simple to-do list app ✨ Store

    to-dos in Cloud Firestore ✨ Real-time updates across devices ✨ Use without user account (“try before you buy”) ✨ Sign in with Email/Password ✨ Sign in with Apple ✨ Feature flags using Remote Config ✨ There is an Android version as well! (bit.ly/makeitso-android-4)
  3. Run with confidence Crashlytics Performance Monitoring Test Lab App Distribution

    Engage users Analytics Predictions Cloud Messaging Remote Config A/B Testing Dynamic Links In-app Messaging Develop apps faster Auth Cloud Functions Cloud Firestore Hosting ML Kit Realtime Database Cloud Storage bit.ly/what-is-firebase Extensions Machine Learning
  4. Adding Firebase to a Swi!UI app Exercise 1. Create a

    new Firebase project 2. Add your app to the new Firebase project 3. Download GoogleServices-Info.plist to your app
  5. SwiftUI 2: No more AppDelegate! import SwiftUI @main struct MakeItSoApp:

    App { var body: some Scene { WindowGroup { TodosListView() } } }
  6. SwiftUI 2: No more AppDelegate! import SwiftUI @main struct MakeItSoApp:

    App { var body: some Scene { WindowGroup { TodosListView() } } }
  7. Solution 1: use initialiser import SwiftUI @main struct MakeItSoApp: App

    { var body: some Scene { WindowGroup { TodosListView() } } }
  8. Solution 1: use initialiser import SwiftUI @main struct MakeItSoApp: App

    { var body: some Scene { WindowGroup { TodosListView() } } } init() { FirebaseApp.configure() }
  9. Solution 2: use UIApplicationDelegateAdaptor import SwiftUI import Firebase class AppDelegate:

    NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) !" Bool { FirebaseApp.configure() return true } } @main struct MakeItSoApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
  10. Solution 2: use UIApplicationDelegateAdaptor import SwiftUI import Firebase class AppDelegate:

    NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) !" Bool { FirebaseApp.configure() return true } } @main struct MakeItSoApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
  11. Solution 2: use UIApplicationDelegateAdaptor import SwiftUI import Firebase class AppDelegate:

    NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) !" Bool { FirebaseApp.configure() return true } } @main struct MakeItSoApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
  12. Solution 2: use UIApplicationDelegateAdaptor import SwiftUI import Firebase class AppDelegate:

    NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) !" Bool { FirebaseApp.configure() return true } } @main struct MakeItSoApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
  13. Watch the scene phase Handle deep links Continue user activities

    Learn more pete!riese.dev/ultimate-guide-to-swi"ui2-application-lifecycle/
  14. bird_type: airspeed: coconut_capacity: isNative: icon: vector: distances_traveled: "swallow" 42.733 0.62

    false <binary data> { x: 36.4255, y: 25.1442, z: 18.8816 } [42, 39, 12, 42] Document
  15. struct Todo { @DocumentID var docId: String? var id: String?

    = UUID().uuidString var title: String var completed: Bool = false var userId: String? } Data Model
  16. class TodosRepository: ObservableObject { @Injected var db: Firestore private var

    listenerRegistration: ListenerRegistration? @Published var todos = [Todo]() func subscribe() { if listenerRegistration !# nil { unsubscribe() } listenerRegistration = db.collection("todos") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$todos = documents.compactMap { queryDocumentSnapshot in let docId = queryDocumentSnapshot.documentID Real-time Sync w/ Snapshot Listeners
  17. class TodosRepository: ObservableObject { @Injected var db: Firestore private var

    listenerRegistration: ListenerRegistration? @Published var todos = [Todo]() func subscribe() { if listenerRegistration !# nil { unsubscribe() } listenerRegistration = db.collection("todos") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$todos = documents.compactMap { queryDocumentSnapshot in let docId = queryDocumentSnapshot.documentID Real-time Sync w/ Snapshot Listeners
  18. class TodosRepository: ObservableObject { @Injected var db: Firestore private var

    listenerRegistration: ListenerRegistration? @Published var todos = [Todo]() func subscribe() { if listenerRegistration !# nil { unsubscribe() } listenerRegistration = db.collection("todos") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$todos = documents.compactMap { queryDocumentSnapshot in let docId = queryDocumentSnapshot.documentID Real-time Sync w/ Snapshot Listeners
  19. class TodosRepository: ObservableObject { @Injected var db: Firestore private var

    listenerRegistration: ListenerRegistration? @Published var todos = [Todo]() func subscribe() { if listenerRegistration !# nil { unsubscribe() } listenerRegistration = db.collection("todos") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$todos = documents.compactMap { queryDocumentSnapshot in let docId = queryDocumentSnapshot.documentID Real-time Sync w/ Snapshot Listeners
  20. func subscribe() { if listenerRegistration !# nil { unsubscribe() }

    listenerRegistration = db.collection("todos") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$todos = documents.compactMap { queryDocumentSnapshot in let docId = queryDocumentSnapshot.documentID let data = queryDocumentSnapshot.data() let id = queryDocumentSnapshot["id"] as? String !% "" let title = data["title"] as? String !% "" let completed = data["completed"] as? Bool !% false let userId = data["userId"] as? String !% "" return Todo(docId: docId, id: id, title: title, completed: completed, userId: userId) } } Real-time Sync w/ Snapshot Listeners
  21. func subscribe() { if listenerRegistration !# nil { unsubscribe() }

    listenerRegistration = db.collection("todos") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$todos = documents.compactMap { queryDocumentSnapshot in let docId = queryDocumentSnapshot.documentID let data = queryDocumentSnapshot.data() let id = queryDocumentSnapshot["id"] as? String !% "" let title = data["title"] as? String !% "" let completed = data["completed"] as? Bool !% false let userId = data["userId"] as? String !% "" return Todo(docId: docId, id: id, title: title, completed: completed, userId: userId) } } Real-time Sync w/ Snapshot Listeners
  22. func subscribe() { if listenerRegistration !# nil { unsubscribe() }

    listenerRegistration = db.collection("todos") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$todos = documents.compactMap { queryDocumentSnapshot in let docId = queryDocumentSnapshot.documentID let data = queryDocumentSnapshot.data() let id = queryDocumentSnapshot["id"] as? String !% "" let title = data["title"] as? String !% "" let completed = data["completed"] as? Bool !% false let userId = data["userId"] as? String !% "" return Todo(docId: docId, id: id, title: title, completed: completed, userId: userId) } } Real-time Sync w/ Snapshot Listeners Can we do better?
  23. func subscribe() { if listenerRegistration !# nil { unsubscribe() }

    listenerRegistration = db.collection("todos") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$todos = documents.compactMap { queryDocumentSnapshot in let docId = queryDocumentSnapshot.documentID let data = queryDocumentSnapshot.data() let id = queryDocumentSnapshot["id"] as? String !% "" let title = data["title"] as? String !% "" let completed = data["completed"] as? Bool !% false let userId = data["userId"] as? String !% "" return Todo(docId: docId, id: id, title: title, completed: completed, userId: userId) } } Real-time Sync w/ Snapshot Listeners
  24. func subscribe() { if listenerRegistration !# nil { unsubscribe() }

    listenerRegistration = db.collection("todos") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$todos = documents.compactMap { queryDocumentSnapshot in let result = Result { try queryDocumentSnapshot.data(as: Todo.self) } switch result { case .success(let todo): return todo case .failure(let error): !& handle error return nil } } } Real-time Sync w/ Snapshot Listeners
  25. Sync Data with Firestore 1. Add Snapshot listeners and display

    data in the UI 2. Add new todo items via the app 3. Update todo items via the app 4.Delete todo items via the app Exercise
  26. struct BookShelfView: View { @FirestoreQuery( collectionPath: "todos", predicates: [ .where("userId",

    isEqualTo: userId), ] ) var todos: Result<[Todo], Error> @State var userId = "F18EBA5E" var body: some View { List(todos) { todo in Text(todo.title) } } } Firestore Property Wrapper Firebase 8.9.0 @FloWritesCode @mo%enditlevsen Thanks to
  27. Sign the user in Update the data model Secure users’

    data How to implement Firebase Authentication?
  28. SignInWithAppleButton( onRequest: { !!( }, onCompletion: { result in !!(

    let appleIDToken = appleIDCredential.identityToken let idTokenString = String(data: appleIDToken, encoding: .utf8) let credential = OAuthProvider.credential(withProviderID: “apple.com", idToken: idTokenString, rawNonce: nonce) do { try await Auth.auth().signIn(with: credential) } catch { !& handle error } } ).frame(width: 280, height: 45, alignment: .center) Sign in with Apple
  29. SignInWithAppleButton( onRequest: { !!( }, onCompletion: { result in !!(

    let appleIDToken = appleIDCredential.identityToken let idTokenString = String(data: appleIDToken, encoding: .utf8) let credential = OAuthProvider.credential(withProviderID: “apple.com", idToken: idTokenString, rawNonce: nonce) do { try await Auth.auth().signIn(with: credential) } catch { !& handle error } } ).frame(width: 280, height: 45, alignment: .center) Sign in with Apple
  30. SignInWithAppleButton( onRequest: { !!( }, onCompletion: { result in !!(

    let appleIDToken = appleIDCredential.identityToken let idTokenString = String(data: appleIDToken, encoding: .utf8) let credential = OAuthProvider.credential(withProviderID: “apple.com", idToken: idTokenString, rawNonce: nonce) do { try await Auth.auth().signIn(with: credential) } catch { !& handle error } } ).frame(width: 280, height: 45, alignment: .center) Sign in with Apple
  31. SignInWithAppleButton( onRequest: { !!( }, onCompletion: { result in !!(

    let appleIDToken = appleIDCredential.identityToken let idTokenString = String(data: appleIDToken, encoding: .utf8) let credential = OAuthProvider.credential(withProviderID: “apple.com", idToken: idTokenString, rawNonce: nonce) do { try await Auth.auth().signIn(with: credential) } catch { !& handle error } } ).frame(width: 280, height: 45, alignment: .center) Sign in with Apple
  32. SignInWithAppleButton( onRequest: { !!( }, onCompletion: { result in !!(

    let appleIDToken = appleIDCredential.identityToken let idTokenString = String(data: appleIDToken, encoding: .utf8) let credential = OAuthProvider.credential(withProviderID: “apple.com", idToken: idTokenString, rawNonce: nonce) do { try await Auth.auth().signIn(with: credential) } catch { !& handle error } } ).frame(width: 280, height: 45, alignment: .center) Sign in with Apple
  33. SignInWithAppleButton( onRequest: { !!( }, onCompletion: { result in !!(

    let appleIDToken = appleIDCredential.identityToken let idTokenString = String(data: appleIDToken, encoding: .utf8) let credential = OAuthProvider.credential(withProviderID: “apple.com", idToken: idTokenString, rawNonce: nonce) do { try await Auth.auth().signIn(with: credential) } catch { !& handle error } } ).frame(width: 280, height: 45, alignment: .center) Sign in with Apple Firebase SDK
  34. let query = db.collection(“todos") .whereField("userId", isEqualTo: self.userId) query .addSnapshotListener {

    [weak self] (querySnapsho guard let documents = querySnapshot!$documents els Signed in user
  35. Implementing User Authentication 1. Add Anonymous Authentication to the app

    2. Add Email and Password Authentication to the app 3. Add Sign in with Apple Exercise
  36. rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match

    /{document=**} { allow read, write: if request.time < timestamp.date(2022, 8, 26); } } } Security Rules Time-based security == no security! Don’t do this at home!
  37. rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match

    /{document=**} { allow create: if request.auth !# null; allow read, update, delete: if request.auth !# null !* resource.data.userId !' request.auth.uid; } } } Security Rules Only signed-in users can create new documents Only owners may read and modify a document
  38. Securing Your Users’ Data with Security Rules 1. Update the

    Security Rule so only authenticated users can create todo items 2. Update the Security Rule so only authenticated users can create todo items Exercise
  39. Popular use cases • Activate banner for New Year’s resolution

    promo at midnight • Drive iOS adoption by offering a 20% discount to iOS users (and 10% to Android users) • Use staged rollout (10% > 25% > 75% > 100%) for the new search feature • Show different home screen content to users in USA vs the UK • Show customised feed based on user’s preferences for particular topic / category • Engage users who are predicted to churn • Show less ads to users who are predicted to spend in your app • Slowly roll out migration to a new API (API URL defined as a RC key) • Define entire game levels as JSON/XML config values. Great for fixing gameplay! • Host static assets on Firebase Hosting and reference them dynamically in your game using Remote Config keys • Disable features at runtime that might be causing high number of crashes
  40. Set up Remote Config import FirebaseRemoteConfig private var remoteConfig: RemoteConfig

    func setupRemoteConfig() { let remoteConfig = RemoteConfig.remoteConfig() let settings = RemoteConfigSettings() settings.minimumFetchInterval = 0 remoteConfig.configSettings = settings remoteConfig.setDefaults(fromPlist: "RemoteConfigDefaults") }
  41. Set up Remote Config import FirebaseRemoteConfig private var remoteConfig: RemoteConfig

    func setupRemoteConfig() { let remoteConfig = RemoteConfig.remoteConfig() let settings = RemoteConfigSettings() settings.minimumFetchInterval = 0 remoteConfig.configSettings = settings remoteConfig.setDefaults(fromPlist: "RemoteConfigDefaults") } Amount of time before you can fetch again after a successful fetch Don’t do this at home!
  42. Set up Remote Config import FirebaseRemoteConfig private var remoteConfig: RemoteConfig

    func setupRemoteConfig() { let remoteConfig = RemoteConfig.remoteConfig() #if DEBUG let settings = RemoteConfigSettings() settings.minimumFetchInterval = 0 remoteConfig.configSettings = settings #endif remoteConfig.setDefaults(fromPlist: "RemoteConfigDefaults") } Amount of time before you can fetch again after a successful fetch Do THIS at home!
  43. Set up Remote Config import FirebaseRemoteConfig private var remoteConfig: RemoteConfig

    func setupRemoteConfig() { let remoteConfig = RemoteConfig.remoteConfig() #if DEBUG let settings = RemoteConfigSettings() settings.minimumFetchInterval = 0 remoteConfig.configSettings = settings #endif remoteConfig.setDefaults(fromPlist: "RemoteConfigDefaults") }
  44. Fetch and apply a value func fetchConfigutation() { remoteConfig.fetch {

    (status, error) !" Void in if status !' .success { print("Configuration fetched!") self.remoteConfig.activate { changed, error in let value = remoteConfig.configValue(forKey: "key") !& apply configuration } } else { print("Configuration not fetched") print("Error: \(error!$localizedDescription !% "No error available.")") } } }
  45. Fetch and apply a value func fetchConfigutation() { remoteConfig.fetch {

    (status, error) !" Void in if status !' .success { print("Configuration fetched!") self.remoteConfig.activate { changed, error in let value = remoteConfig.configValue(forKey: "key") !& apply configuration } } else { print("Configuration not fetched") print("Error: \(error!$localizedDescription !% "No error available.")") } } }
  46. Fetch and apply a value func fetchConfigutation() { remoteConfig.fetch {

    (status, error) !" Void in if status !' .success { print("Configuration fetched!") self.remoteConfig.activate { changed, error in let value = remoteConfig.configValue(forKey: "key") !& apply configuration } } else { print("Configuration not fetched") print("Error: \(error!$localizedDescription !% "No error available.")") } } }
  47. Fetch and apply a value w/ async/await func fetchConfigutation() async

    { do { let status = try await remoteConfig.fetch() if status !' .success { print("Configuration fetched!") try await remoteConfig.activate() let value = remoteConfig.configValue(forKey: "") } } catch { !& handle error } }
  48. Fetch and apply a value w/ async/await func fetchConfigutation() async

    { do { let status = try await remoteConfig.fetch() if status !' .success { print("Configuration fetched!") try await remoteConfig.activate() let value = remoteConfig.configValue(forKey: "") } } catch { !& handle error } }
  49. Fetch and apply a value w/ async/await func fetchConfigutation() async

    { do { let status = try await remoteConfig.fetch() if status !' .success { print("Configuration fetched!") try await remoteConfig.activate() let value = remoteConfig.configValue(forKey: "") } } catch { !& handle error } }
  50. Implementing Remote Con"g 1. Create a con#guration to hide/show the

    details bu$on for each todo item 2. Fetch the con#guration when your app sta", and apply to the UI 3. Launch this con#guration to 10% of your users only Exercise