Slide 1

Slide 1 text

Peter Friese | Developer Advocate, Firebase Marina Coelho | Developer Relations Engineer, Firebase  + Swi!UI & Firebase Workshop @coelho_dev @pete!riese

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

No content

Slide 4

Slide 4 text

What we’re going to build

Slide 5

Slide 5 text

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)

Slide 6

Slide 6 text

Demo

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

Run with confidence Engage users Develop apps faster

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

Adding Firebase to a Swi!UI app Exercise

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

Application Lifecycle SwiftUI 2 Photo by Thor Alvis on Unsplash

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Watch the scene phase Handle deep links Continue user activities Do more with the new life cycle

Slide 22

Slide 22 text

Watch the scene phase Handle deep links Continue user activities Learn more pete!riese.dev/ultimate-guide-to-swi"ui2-application-lifecycle/

Slide 23

Slide 23 text

Firestore

Slide 24

Slide 24 text

No content

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

One-Time Fetches Offline Mode Effortless Syncing

Slide 27

Slide 27 text

bird_type: airspeed: coconut_capacity: isNative: icon: vector: distances_traveled: "swallow" 42.733 0.62 false { x: 36.4255, y: 25.1442, z: 18.8816 } [42, 39, 12, 42] Document

Slide 28

Slide 28 text

Collection Sub-Collection

Slide 29

Slide 29 text

Top level collections “todos” collection a single todo document

Slide 30

Slide 30 text

struct Todo { @DocumentID var docId: String? var id: String? = UUID().uuidString var title: String var completed: Bool = false var userId: String? } Data Model

Slide 31

Slide 31 text

TodosListView TodoDetailsView TodoListRowView Source of Truth todos: [Todo] @StateObject @Binding Todo Todo Review: Architecture

Slide 32

Slide 32 text

TodosListView TodoDetailsView TodoListRowView Source of Truth todos: [Todo] @StateObject @Binding Todo Todo Review: Architecture Snapshot Listener

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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?

Slide 40

Slide 40 text

(Yes, we can)

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Learn more h#ps://pete!riese.dev/$restore-codable-the-comprehensive-guide/

Slide 44

Slide 44 text

Sync Data with Firestore Exercise

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Authentication Photo by Conve"Kit on Unsplash

Slide 48

Slide 48 text

Authentication Photo by Eduardo Soares on Unsplash

Slide 49

Slide 49 text

Authentication Photo by Victor Freitas on Unsplash

Slide 50

Slide 50 text

Slide 51

Slide 51 text

Sign the user in Update the data model Secure users’ data How to implement Firebase Authentication?

Slide 52

Slide 52 text

Anonymous Authentication “Guest” accounts, rather

Slide 53

Slide 53 text

func signIn() { registerStateListener() if Auth.auth().currentUser !' nil { Auth.auth().signInAnonymously() } } Anonymous Authentication

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

All todos are stored in one single collection Which user do they belong to?

Slide 62

Slide 62 text

let query = db.collection(“todos") .whereField("userId", isEqualTo: self.userId) query .addSnapshotListener { [weak self] (querySnapsho guard let documents = querySnapshot!$documents els Signed in user

Slide 63

Slide 63 text

Implementing User Authentication Exercise

Slide 64

Slide 64 text

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

Slide 65

Slide 65 text

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!

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

Securing Your Users’ Data with Security Rules Exercise

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

Remote Con"g Photo by Ash Edmonds on Unsplash

Slide 70

Slide 70 text

Deploy Feature Flags with Progressive Rollouts

Slide 71

Slide 71 text

Make changes to your app without publishing an App Update

Slide 72

Slide 72 text

Target speci#c user segments

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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!

Slide 76

Slide 76 text

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!

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

Implementing Remote Con"g Exercise

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

The End Photo by Paul Hudson (@twostraws) on Twi$er

Slide 87

Slide 87 text

The End

Slide 88

Slide 88 text

The End ) (for real

Slide 89

Slide 89 text

The End (for real now - why don’t you go and grab a drink )