Slide 1

Slide 1 text

Peter Friese | Developer Advocate, Firebase Charlo!e Liang | So"ware Engineer, Firebase  + Firebase for Apple Developers @charlo!eCLiang @pete#riese

Slide 2

Slide 2 text

Architecture Swi!UI 2 Life Cycle Firebase Firestore Authentication Combine async/await Completion block WidgetKit

Slide 3

Slide 3 text

Architecture For Data-Driven Apps Photo by Lance Anderson on Unsplash

Slide 4

Slide 4 text

#one-book-a-week-challenge

Slide 5

Slide 5 text

No content

Slide 6

Slide 6 text

Drill-down navigation Three-column layout (iPad / Mac) Single source of truth Driving factors UI always in sync

Slide 7

Slide 7 text

UI always in sync

Slide 8

Slide 8 text

BookShelfView BookEditView BookDetailsView BookRowView Source of Truth books: [Book] @ObservableObject @Binding Book Book Book

Slide 9

Slide 9 text

Challenge #1 This needs to be a binding But this isn’t a list of bindings

Slide 10

Slide 10 text

struct BookShelfView: View { @Binding var bookShelf: BookShelf var body: some View { List { ForEach(Array(bookShelf.books.enumerated()), id: \.element.id) { index, item in BookRowView(book: $bookShelf.books[index]) } .onDelete { indexSet in bookShelf.books.remove(atOffsets: indexSet) } } .navigationTitle(bookShelf.title) } } Solution #1: iterate over enumerated items !

Slide 11

Slide 11 text

struct BookShelfView: View { @Binding var bookShelf: BookShelf var body: some View { List { ForEach($bookShelf.books) { $book in BookRowView(book: $book) } .onDelete { indexSet in bookShelf.books.remove(atOffsets: indexSet) } } .navigationTitle(bookShelf.title) } } Solution #1: use list bindings Everything is now bindable

Slide 12

Slide 12 text

Learn more h"ps://pete#riese.dev/swi!ui-list-item-bindings-behind-the-scenes/

Slide 13

Slide 13 text

How to update only when the user commits? Challenge #2

Slide 14

Slide 14 text

struct BookEditView: View { @Binding var book: Book @ObservedObject var bookEditViewModel: BookEditViewModel init(book: Binding) { self._book = book self.bookEditViewModel = BookEditViewModel(book: book.wrappedValue) } var body: some View { TextField("Book title", text: $bookEditViewModel.book.title) } func save() { self.book = bookEditViewModel.book dismiss() } } Solution 2: use inner @ObservableObject

Slide 15

Slide 15 text

struct BookEditView: View { @Binding var book: Book @ObservedObject var bookEditViewModel: BookEditViewModel init(book: Binding) { self._book = book self.bookEditViewModel = BookEditViewModel(book: book.wrappedValue) } var body: some View { TextField("Book title", text: $bookEditViewModel.book.title) } func save() { self.book = bookEditViewModel.book dismiss() } } Solution 2: use inner @ObservableObject

Slide 16

Slide 16 text

struct BookEditView: View { @Binding var book: Book @ObservedObject var bookEditViewModel: BookEditViewModel init(book: Binding) { self._book = book self.bookEditViewModel = BookEditViewModel(book: book.wrappedValue) } var body: some View { TextField("Book title", text: $bookEditViewModel.book.title) } func save() { self.book = bookEditViewModel.book dismiss() } } Solution 2: use inner @ObservableObject

Slide 17

Slide 17 text

struct BookEditView: View { @Binding var book: Book @ObservedObject var bookEditViewModel: BookEditViewModel init(book: Binding) { self._book = book self.bookEditViewModel = BookEditViewModel(book: book.wrappedValue) } var body: some View { TextField("Book title", text: $bookEditViewModel.book.title) } func save() { self.book = bookEditViewModel.book dismiss() } } Solution 2: use inner @ObservableObject

Slide 18

Slide 18 text

Solution 2

Slide 19

Slide 19 text

class BookEditViewModel: ObservableObject { @Published var book: Book @Published var isISBNValid: Bool = true init(book: Book) { self.book = book self.$book .map { checkISBN(isbn: $0.isbn) } .assign(to: &$isISBNValid) } } Solution 2: use inner @ObservableObject Bonus: use Combine to perform validation

Slide 20

Slide 20 text

Learn more about Building SwiftUI Components h"ps://www.youtube.com/c/pete#riese

Slide 21

Slide 21 text

Architecture Swi!UI 2 Life Cycle Firebase Firestore Authentication Combine async/await Completion block WidgetKit

Slide 22

Slide 22 text

No content

Slide 23

Slide 23 text

Run with confidence Engage users Develop apps faster

Slide 24

Slide 24 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 25

Slide 25 text

No content

Slide 26

Slide 26 text

No content

Slide 27

Slide 27 text

No content

Slide 28

Slide 28 text

#protip: Put it beneath Asset.xcasset

Slide 29

Slide 29 text

Don’t forget to add to all the targets!

Slide 30

Slide 30 text

Swift Package Manager now officially supported! h"ps://github.com/%rebase/%rebase-ios-sdk

Slide 31

Slide 31 text

!

Slide 32

Slide 32 text

Application Lifecycle SwiftUI 2 Photo by Thor Alvis on Unsplash

Slide 33

Slide 33 text

SwiftUI 2: No more AppDelegate! import SwiftUI @main struct BookShelfApp: App { @StateObject var store = BookShelfStore(shelves: BookShelf.samples) var body: some Scene { WindowGroup { NavigationView { BookShelvesView(store: store) Text("Select a shelf to see its books") Text("Select a book to see its details") } } } }

Slide 34

Slide 34 text

SwiftUI 2: No more AppDelegate! import SwiftUI @main struct BookShelfApp: App { @StateObject var store = BookShelfStore(shelves: BookShelf.samples) var body: some Scene { WindowGroup { NavigationView { BookShelvesView(store: store) Text("Select a shelf to see its books") Text("Select a book to see its details") } } } }

Slide 35

Slide 35 text

Solution 1: use initialiser import SwiftUI @main struct BookShelfApp: App { @StateObject var store = BookShelfStore(shelves: BookShelf.samples) var body: some Scene { WindowGroup { NavigationView { BookShelvesView(store: store) Text("Select a shelf to see its books") Text("Select a book to see its details") }

Slide 36

Slide 36 text

Solution 1: use initialiser import SwiftUI @main struct BookShelfApp: App { @StateObject var store = BookShelfStore(shelves: BookShelf.samples) var body: some Scene { WindowGroup { NavigationView { BookShelvesView(store: store) Text("Select a shelf to see its books") Text("Select a book to see its details") } init() { FirebaseApp.configure() }

Slide 37

Slide 37 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 BookShelfApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

Slide 38

Slide 38 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 BookShelfApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

Slide 39

Slide 39 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 BookShelfApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

Slide 40

Slide 40 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 BookShelfApp: App { @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Architecture Swi!UI 2 Life Cycle Firebase Firestore Authentication Combine async/await Completion block WidgetKit

Slide 44

Slide 44 text

Firestore

Slide 45

Slide 45 text

No content

Slide 46

Slide 46 text

No content

Slide 47

Slide 47 text

One-Time Fetches Offline Mode Effortless Syncing

Slide 48

Slide 48 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 49

Slide 49 text

Collection Sub-Collection

Slide 50

Slide 50 text

Top level collections “books” collection a single book document

Slide 51

Slide 51 text

struct Book: Identifiable { var id = String? var shelfId: String? var userId: String? var title: String var author: String var isbn: String var pages: Int var isRead: Bool = false var coverEditionKey: String? } Data Model

Slide 52

Slide 52 text

func fetchBook(documentId: String) { let docRef = Firestore.firestore().collection("books").document(documentId) docRef.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") } else { if let document = document { let id = document.documentID let data = document.data() let title = data?["title"] as? String !# "" let numberOfPages = data?["numberOfPages"] as? Int !# 0 let author = data?["author"] as? String !# "" self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author) } } } } Fetching a document from Firestore

Slide 53

Slide 53 text

func fetchBook(documentId: String) { let docRef = Firestore.firestore().collection("books").document(documentId) docRef.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") } else { if let document = document { let id = document.documentID let data = document.data() let title = data?["title"] as? String !# "" let numberOfPages = data?["numberOfPages"] as? Int !# 0 let author = data?["author"] as? String !# "" self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author) } } } } Fetching a document from Firestore

Slide 54

Slide 54 text

func fetchBook(documentId: String) { let docRef = Firestore.firestore().collection("books").document(documentId) docRef.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") } else { if let document = document { let id = document.documentID let data = document.data() let title = data?["title"] as? String !# "" let numberOfPages = data?["numberOfPages"] as? Int !# 0 let author = data?["author"] as? String !# "" self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author) } } } } Fetching a document from Firestore

Slide 55

Slide 55 text

func fetchBook(documentId: String) { let docRef = Firestore.firestore().collection("books").document(documentId) docRef.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") } else { if let document = document { let id = document.documentID let data = document.data() let title = data?["title"] as? String !# "" let numberOfPages = data?["numberOfPages"] as? Int !# 0 let author = data?["author"] as? String !# "" self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author) } } } } Fetching a document from Firestore

Slide 56

Slide 56 text

func fetchBook(documentId: String) { let docRef = Firestore.firestore().collection("books").document(documentId) docRef.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") } else { if let document = document { let id = document.documentID let data = document.data() let title = data?["title"] as? String !# "" let numberOfPages = data?["numberOfPages"] as? Int !# 0 let author = data?["author"] as? String !# "" self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author) } } } } Fetching a document from Firestore

Slide 57

Slide 57 text

func fetchBook(documentId: String) { let docRef = Firestore.firestore().collection("books").document(documentId) docRef.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") } else { if let document = document { let id = document.documentID let data = document.data() let title = data?["title"] as? String !# "" let numberOfPages = data?["numberOfPages"] as? Int !# 0 let author = data?["author"] as? String !# "" self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author) } } } } Fetching a document from Firestore

Slide 58

Slide 58 text

func fetchBook(documentId: String) { let docRef = Firestore.firestore().collection("books").document(documentId) docRef.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") } else { if let document = document { let id = document.documentID let data = document.data() let title = data?["title"] as? String !# "" let numberOfPages = data?["numberOfPages"] as? Int !# 0 let author = data?["author"] as? String !# "" self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author) } } } } Fetching a document from Firestore if let document = document { let id = document.documentID let data = document.data() let title = data?["title"] as? String !# "" let numberOfPages = data?["numberOfPages"] as? Int !# 0 let author = data?["author"] as? String !# "" self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author) } Can we do better?

Slide 59

Slide 59 text

(Yes, we can)

Slide 60

Slide 60 text

func fetchBook(documentId: String) { let docRef = Firestore.firestore().collection("books").document(documentId) docRef.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") } else { if let document = document { let id = document.documentID let data = document.data() let title = data?["title"] as? String !# "" let numberOfPages = data?["numberOfPages"] as? Int !# 0 let author = data?["author"] as? String !# "" self.book = Book(id:id, title: title, numberOfPages: numberOfPages, author: author) } } } } Fetching a document from Firestore

Slide 61

Slide 61 text

func fetchBook(documentId: String) { let docRef = Firestore.firestore().collection("books").document(documentId) docRef.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") } else { } } } Mapping data using Codable if let document = document { do { self.book = try document.data(as: Book.self) } catch { print(error) } }

Slide 62

Slide 62 text

func fetchBook(documentId: String) { let docRef = Firestore.firestore().collection("books").document(documentId) docRef.getDocument { document, error in if let error = error as NSError? { print("Error getting document: \(error.localizedDescription)") } else { } } } Mapping data using Codable if let document = document { do { self.book = try document.data(as: Book.self) } catch { print(error) } }

Slide 63

Slide 63 text

BookShelfView BookEditView BookDetailsView BookRowView Source of Truth books: [Book] @ObservableObject @Binding Book Book Book Review: Architecture

Slide 64

Slide 64 text

BookShelfView BookDetailsView BookRowView Source of Truth books: [Book] @ObservableObject @Binding Book Book Review: Architecture Snapshot Listener

Slide 65

Slide 65 text

class BookStore: ObservableObject { var db = Firestore.firestore() private var listenerRegistration: ListenerRegistration? @Published var books = [Book]() func subscribe() { listenerRegistration = db.collection("books") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$books = documents.compactMap { queryDocumentSnapshot in let result = Result { try queryDocumentSnapshot.data(as: Book.self) } switch result { case .success(let book): if let book = book { return book Fetching a collection of documents

Slide 66

Slide 66 text

class BookStore: ObservableObject { var db = Firestore.firestore() private var listenerRegistration: ListenerRegistration? @Published var books = [Book]() func subscribe() { listenerRegistration = db.collection("books") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$books = documents.compactMap { queryDocumentSnapshot in let result = Result { try queryDocumentSnapshot.data(as: Book.self) } switch result { case .success(let book): if let book = book { return book Fetching a collection of documents

Slide 67

Slide 67 text

class BookStore: ObservableObject { var db = Firestore.firestore() private var listenerRegistration: ListenerRegistration? @Published var books = [Book]() func subscribe() { listenerRegistration = db.collection("books") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$books = documents.compactMap { queryDocumentSnapshot in let result = Result { try queryDocumentSnapshot.data(as: Book.self) } switch result { case .success(let book): if let book = book { return book Fetching a collection of documents

Slide 68

Slide 68 text

class BookStore: ObservableObject { var db = Firestore.firestore() private var listenerRegistration: ListenerRegistration? @Published var books = [Book]() func subscribe() { listenerRegistration = db.collection("books") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$books = documents.compactMap { queryDocumentSnapshot in let result = Result { try queryDocumentSnapshot.data(as: Book.self) } switch result { case .success(let book): if let book = book { return book Fetching a collection of documents

Slide 69

Slide 69 text

class BookStore: ObservableObject { var db = Firestore.firestore() private var listenerRegistration: ListenerRegistration? @Published var books = [Book]() func subscribe() { listenerRegistration = db.collection("books") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$books = documents.compactMap { queryDocumentSnapshot in let result = Result { try queryDocumentSnapshot.data(as: Book.self) } switch result { case .success(let book): if let book = book { return book Fetching a collection of documents

Slide 70

Slide 70 text

class BookStore: ObservableObject { var db = Firestore.firestore() private var listenerRegistration: ListenerRegistration? @Published var books = [Book]() func subscribe() { listenerRegistration = db.collection("books") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$books = documents.compactMap { queryDocumentSnapshot in let result = Result { try queryDocumentSnapshot.data(as: Book.self) } switch result { case .success(let book): if let book = book { return book Fetching a collection of documents

Slide 71

Slide 71 text

class BookStore: ObservableObject { var db = Firestore.firestore() private var listenerRegistration: ListenerRegistration? @Published var books = [Book]() func subscribe() { listenerRegistration = db.collection("books") .addSnapshotListener { [weak self] (querySnapshot, error) in guard let documents = querySnapshot!$documents else { return } self!$books = documents.compactMap { queryDocumentSnapshot in let result = Result { try queryDocumentSnapshot.data(as: Book.self) } switch result { case .success(let book): if let book = book { return book Fetching a collection of documents

Slide 72

Slide 72 text

No content

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

import Foundation import Combine import Firebase import FirebaseFirestoreSwift import os class BookStore: ObservableObject { !% MARK: - Dependencies var db = Firestore.firestore() !% MARK: - Publishers @Published var user: User? @Published var books = [Book]() !% MARK: - Private attributes @Published private var userId: String = "unknown" private var listenerRegistration: ListenerRegistration? private var cancellables = Set() Fetching a collection of documents

Slide 75

Slide 75 text

!% A Book value could not be initialized from the DocumentSnapshot. switch error { case DecodingError.typeMismatch(_, let context): self!$logger.debug("\(error.localizedDescription): \ (context.debugDescription)") case DecodingError.valueNotFound(_, let context): self!$logger.debug("\(error.localizedDescription): \ (context.debugDescription)") case DecodingError.keyNotFound(_, let context): self!$logger.debug("\(error.localizedDescription): \ (context.debugDescription)") case DecodingError.dataCorrupted(let key): self!$logger.debug("\(error.localizedDescription): \ (key.debugDescription)") default: self!$logger.debug("Error decoding document: \ (error.localizedDescription)") } return nil } } } } Fetching a collection of documents

Slide 76

Slide 76 text

!% A Book value could not be initialized from the DocumentSnapshot. switch error { case DecodingError.typeMismatch(_, let context): self!$logger.debug("\(error.localizedDescription): \ (context.debugDescription)") case DecodingError.valueNotFound(_, let context): self!$logger.debug("\(error.localizedDescription): \ (context.debugDescription)") case DecodingError.keyNotFound(_, let context): self!$logger.debug("\(error.localizedDescription): \ (context.debugDescription)") case DecodingError.dataCorrupted(let key): self!$logger.debug("\(error.localizedDescription): \ (key.debugDescription)") default: self!$logger.debug("Error decoding document: \ (error.localizedDescription)") } return nil } } } } Fetching a collection of documents about 100 lines of code

Slide 77

Slide 77 text

struct BookShelfView: View { @FirestoreQuery( collectionPath: "books", predicates: [ .where("userId", isEqualTo: userId), ] ) var books: Result<[Book], Error> @State var userId = "F18EBA5E" var body: some View { List(books) { book in Text(book.title) } } } Firestore Property Wrapper Firebase 8.9.0 @FloWritesCode @mo&enditlevsen Thanks to

Slide 78

Slide 78 text

Architecture Swi!UI 2 Life Cycle Firebase Firestore Authentication Combine async/await Completion block WidgetKit

Slide 79

Slide 79 text

Driving User Engagement Photo by ROBIN WORRALL on Unsplash

Slide 80

Slide 80 text

Driving User Engagement Noti%cations Share Extensions WidgetKit

Slide 81

Slide 81 text

Notification Service Extension Noti%cation Service Extension + Firebase Messaging Be!er visualisation of noti%cations Available in watchOS as well

Slide 82

Slide 82 text

Watch the scene phase Handle deep links Continue user activities Learn more h"ps://%rebase.blog/posts/2019/09/fcm-image-noti%cation

Slide 83

Slide 83 text

Firebase + WidgetKit Mini travel blog How to initialise Firebase How to access Firebase services

Slide 84

Slide 84 text

Post Image ShareExtension Widget iOS App Architecture

Slide 85

Slide 85 text

class ShareViewController: SLComposeServiceViewController { override func didSelectPost() { !" get data from share controller context Task { !" write image data to Cloud Storage let _ = try await ref.putDataAsync(self.imageData as Data) let url = try await ref.downloadURL() !" write post data to Firestore let post = Post(description: description !# "", url: url.absoluteString) try db.collection("Posts").document("post").setData(from: post) !" refresh widgets and return to share controller WidgetCenter.shared.reloadAllTimelines() extensionContext!$completeRequest(returningItems: []) } } Firebase in a ShareViewController

Slide 86

Slide 86 text

class ShareViewController: SLComposeServiceViewController { override func didSelectPost() { !" get data from share controller context Task { !" write image data to Cloud Storage let _ = try await ref.putDataAsync(self.imageData as Data) let url = try await ref.downloadURL() !" write post data to Firestore let post = Post(description: description !# "", url: url.absoluteString) try db.collection("Posts").document("post").setData(from: post) !" refresh widgets and return to share controller WidgetCenter.shared.reloadAllTimelines() extensionContext!$completeRequest(returningItems: []) } } Firebase in a ShareViewController

Slide 87

Slide 87 text

class ShareViewController: SLComposeServiceViewController { override func didSelectPost() { !" get data from share controller context Task { !" write image data to Cloud Storage let _ = try await ref.putDataAsync(self.imageData as Data) let url = try await ref.downloadURL() !" write post data to Firestore let post = Post(description: description !# "", url: url.absoluteString) try db.collection("Posts").document("post").setData(from: post) !" refresh widgets and return to share controller WidgetCenter.shared.reloadAllTimelines() extensionContext!$completeRequest(returningItems: []) } } Firebase in a ShareViewController

Slide 88

Slide 88 text

class ShareViewController: SLComposeServiceViewController { override func didSelectPost() { !" get data from share controller context Task { !" write image data to Cloud Storage let _ = try await ref.putDataAsync(self.imageData as Data) let url = try await ref.downloadURL() !" write post data to Firestore let post = Post(description: description !# "", url: url.absoluteString) try db.collection("Posts").document("post").setData(from: post) !" refresh widgets and return to share controller WidgetCenter.shared.reloadAllTimelines() extensionContext!$completeRequest(returningItems: []) } } Firebase in a ShareViewController

Slide 89

Slide 89 text

class ShareViewController: SLComposeServiceViewController { override func didSelectPost() { !" get data from share controller context Task { !" write image data to Cloud Storage let _ = try await ref.putDataAsync(self.imageData as Data) let url = try await ref.downloadURL() !" write post data to Firestore let post = Post(description: description !# "", url: url.absoluteString) try db.collection("Posts").document("post").setData(from: post) !" refresh widgets and return to share controller WidgetCenter.shared.reloadAllTimelines() extensionContext!$completeRequest(returningItems: []) } } Firebase in a ShareViewController

Slide 90

Slide 90 text

class ShareViewController: SLComposeServiceViewController { override func didSelectPost() { !" get data from share controller context Task { !" write image data to Cloud Storage let _ = try await ref.putDataAsync(self.imageData as Data) let url = try await ref.downloadURL() !" write post data to Firestore let post = Post(description: description !# "", url: url.absoluteString) try db.collection("Posts").document("post").setData(from: post) !" refresh widgets and return to share controller WidgetCenter.shared.reloadAllTimelines() extensionContext!$completeRequest(returningItems: []) } } Firebase in a ShareViewController

Slide 91

Slide 91 text

Post Image ShareExtension Widget iOS App Architecture Post Image

Slide 92

Slide 92 text

func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) !" Void) { if FirebaseApp.app() !& nil { FirebaseApp.configure() } Task { let (image, text) = try await PostRepository.getPost(imageName: “image.png") let entry = SimpleEntry( date: Calendar.current.date(byAdding: .minute, value: 15, to: Date())!, configuration: configuration, image: image, text: text ) let timeline = Timeline(entries: [entry], policy: .atEnd) completion(timeline) } } Firebase in a Widget

Slide 93

Slide 93 text

func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) !" Void) { if FirebaseApp.app() !& nil { FirebaseApp.configure() } Task { let (image, text) = try await PostRepository.getPost(imageName: “image.png") let entry = SimpleEntry( date: Calendar.current.date(byAdding: .minute, value: 15, to: Date())!, configuration: configuration, image: image, text: text ) let timeline = Timeline(entries: [entry], policy: .atEnd) completion(timeline) } } Firebase in a Widget

Slide 94

Slide 94 text

func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) !" Void) { if FirebaseApp.app() !& nil { FirebaseApp.configure() } Task { let (image, text) = try await PostRepository.getPost(imageName: “image.png") let entry = SimpleEntry( date: Calendar.current.date(byAdding: .minute, value: 15, to: Date())!, configuration: configuration, image: image, text: text ) let timeline = Timeline(entries: [entry], policy: .atEnd) completion(timeline) } } Firebase in a Widget

Slide 95

Slide 95 text

func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) !" Void) { if FirebaseApp.app() !& nil { FirebaseApp.configure() } Task { let (image, text) = try await PostRepository.getPost(imageName: “image.png") let entry = SimpleEntry( date: Calendar.current.date(byAdding: .minute, value: 15, to: Date())!, configuration: configuration, image: image, text: text ) let timeline = Timeline(entries: [entry], policy: .atEnd) completion(timeline) } } Firebase in a Widget

Slide 96

Slide 96 text

func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) !" Void) { if FirebaseApp.app() !& nil { FirebaseApp.configure() } Task { let (image, text) = try await PostRepository.getPost(imageName: “image.png") let entry = SimpleEntry( date: Calendar.current.date(byAdding: .minute, value: 15, to: Date())!, configuration: configuration, image: image, text: text ) let timeline = Timeline(entries: [entry], policy: .atEnd) completion(timeline) } } Firebase in a Widget

Slide 97

Slide 97 text

import FirebaseStorage import FirebaseStorageSwift import FirebaseFirestore import FirebaseFirestoreSwift struct PostRepository { static func getPost(imageName: String) async throws !" (UIImage, String) { let ref = Storage.storage().reference().child(imageName) let data = try await ref.data(maxSize: 20 * 1024 * 2048) let image = UIImage(data: data)! let db = Firestore.firestore() let docRef = db.collection("Posts").document("post") let document = try await docRef.getDocument() let post = try document.data(as: Post.self) return (image, post.description) Firebase in a Widget

Slide 98

Slide 98 text

import FirebaseStorage import FirebaseStorageSwift import FirebaseFirestore import FirebaseFirestoreSwift struct PostRepository { static func getPost(imageName: String) async throws !" (UIImage, String) { let ref = Storage.storage().reference().child(imageName) let data = try await ref.data(maxSize: 20 * 1024 * 2048) let image = UIImage(data: data)! let db = Firestore.firestore() let docRef = db.collection("Posts").document("post") let document = try await docRef.getDocument() let post = try document.data(as: Post.self) return (image, post.description) Firebase in a Widget

Slide 99

Slide 99 text

import FirebaseStorage import FirebaseStorageSwift import FirebaseFirestore import FirebaseFirestoreSwift struct PostRepository { static func getPost(imageName: String) async throws !" (UIImage, String) { let ref = Storage.storage().reference().child(imageName) let data = try await ref.data(maxSize: 20 * 1024 * 2048) let image = UIImage(data: data)! let db = Firestore.firestore() let docRef = db.collection("Posts").document("post") let document = try await docRef.getDocument() let post = try document.data(as: Post.self) return (image, post.description) Firebase in a Widget

Slide 100

Slide 100 text

import FirebaseStorage import FirebaseStorageSwift import FirebaseFirestore import FirebaseFirestoreSwift struct PostRepository { static func getPost(imageName: String) async throws !" (UIImage, String) { let ref = Storage.storage().reference().child(imageName) let data = try await ref.data(maxSize: 20 * 1024 * 2048) let image = UIImage(data: data)! let db = Firestore.firestore() let docRef = db.collection("Posts").document("post") let document = try await docRef.getDocument() let post = try document.data(as: Post.self) return (image, post.description) Firebase in a Widget

Slide 101

Slide 101 text

Architecture Swi!UI 2 Life Cycle Firebase Firestore Authentication Combine async/await Completion block WidgetKit

Slide 102

Slide 102 text

Authentication Photo by Conve&Kit on Unsplash

Slide 103

Slide 103 text

Authentication Photo by Eduardo Soares on Unsplash

Slide 104

Slide 104 text

Authentication Photo by Victor Freitas on Unsplash

Slide 105

Slide 105 text

Slide 106

Slide 106 text

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

Slide 107

Slide 107 text

Anonymous Authentication “Guest” accounts, rather

Slide 108

Slide 108 text

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

Slide 109

Slide 109 text

No content

Slide 110

Slide 110 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) Auth.auth().signIn(with: credential) { (authResult, error) in if (error !( nil) { !!' } print(“User signed in") dismiss() } } ).frame(width: 280, height: 45, alignment: .center) Sign in with Apple

Slide 111

Slide 111 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) Auth.auth().signIn(with: credential) { (authResult, error) in if (error !( nil) { !!' } print(“User signed in") dismiss() } } ).frame(width: 280, height: 45, alignment: .center) Sign in with Apple

Slide 112

Slide 112 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) Auth.auth().signIn(with: credential) { (authResult, error) in if (error !( nil) { !!' } print(“User signed in") dismiss() } } ).frame(width: 280, height: 45, alignment: .center) Sign in with Apple

Slide 113

Slide 113 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) Auth.auth().signIn(with: credential) { (authResult, error) in if (error !( nil) { !!' } print(“User signed in") dismiss() } } ).frame(width: 280, height: 45, alignment: .center) Sign in with Apple

Slide 114

Slide 114 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) Auth.auth().signIn(with: credential) { (authResult, error) in if (error !( nil) { !!' } print(“User signed in") dismiss() } } ).frame(width: 280, height: 45, alignment: .center) Sign in with Apple Firebase SDK

Slide 115

Slide 115 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) Auth.auth().signIn(with: credential) { (authResult, error) in if (error !( nil) { !!' } print(“User signed in") dismiss() } } ).frame(width: 280, height: 45, alignment: .center) Sign in with Apple Firebase SDK

Slide 116

Slide 116 text

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

Slide 117

Slide 117 text

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

Slide 118

Slide 118 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 119

Slide 119 text

Architecture Swi!UI 2 Life Cycle Firebase Firestore Authentication Combine async/await Completion block WidgetKit

Slide 120

Slide 120 text

Combine

Slide 121

Slide 121 text

auth!$signInAnonymously() let user = auth!$currentUser print("User signed in with user ID: \(user!$uid)") This might be nil

Slide 122

Slide 122 text

auth!$signInAnonymously() let user = auth!$currentUser print("User signed in with user ID: \(user!$uid)") auth!$signInAnonymously { result, error in guard let result = result else { return } print("User signed in with user ID: \(result.user.uid)") } Do this instead

Slide 123

Slide 123 text

auth!$signInAnonymously() let user = auth!$currentUser print("User signed in with user ID: \(user!$uid)") auth!$signInAnonymously { result, error in guard let result = result else { return } print("User signed in with user ID: \(result.user.uid)") } @Published var user: User? !!' auth!$signInAnonymously() .map{ $0.user } .replaceError(with: nil) .assign(to: &$user) Even better

Slide 124

Slide 124 text

Launched with Firebase 8.9.0 h"ps://github.com/%rebase/%rebase-ios-sdk/projects/3

Slide 125

Slide 125 text

Architecture Swi!UI 2 Life Cycle Firebase Firestore Authentication Combine async/await Completion block WidgetKit

Slide 126

Slide 126 text

async/await Photo by Stephen H on Unsplash

Slide 127

Slide 127 text

extension ArticleAnalyser { func process(url: String, completion: @escaping (Article) !" Void) { self.fetchArticle(from: url) { result in switch result { case .failure(let error): print(error.localizedDescription) case .success(let html): self.extractTitle(from: html) { result in switch result { case .failure(let error): print(error.localizedDescription) case .success(let title): self.extractText(from: html) { result in switch result { case .failure(let error): print(error.localizedDescription) case .success(let text): self.extractImage(from: url) { result in Problem: Callback Pyramid of Doom

Slide 128

Slide 128 text

extension AsyncArticleAnalyser { func process(url: String) async throws !" Article { let htmlText = try await fetchArticle(from: url) let text = try await extractText(from: htmlText) let title = try await extractTitle(from: htmlText) let imageUrl = try await extractImage(from: url) let tags = await inferTags(from: text) return Article(url: url, title: title, tags: tags, imageUrlString: imageUrl) } } Solution: Use async/await Swift 5.5

Slide 129

Slide 129 text

func fetchArticle(from url: String) async throws !" String { guard let url = URL(string: url) else { throw AnalyserError.badURL } return try await withUnsafeThrowingContinuation { continuation in URLSession.shared.downloadTask(with: url) { (localUrl, urlResponse, error) in guard let localUrl = localUrl else { continuation.resume(throwing: AnalyserError.badURL) return } if let htmlText = try? String(contentsOf: localUrl) { continuation.resume(returning: htmlText) } } .resume() } } Solution: Use async/await Swift 5.5

Slide 130

Slide 130 text

func fetchArticle(from url: String) async throws !" String { guard let url = URL(string: url) else { throw AnalyserError.badURL } return try await withUnsafeThrowingContinuation { continuation in URLSession.shared.downloadTask(with: url) { (localUrl, urlResponse, error) in guard let localUrl = localUrl else { continuation.resume(throwing: AnalyserError.badURL) return } if let htmlText = try? String(contentsOf: localUrl) { continuation.resume(returning: htmlText) } } .resume() } } Solution: Use async/await Swift 5.5

Slide 131

Slide 131 text

func fetchArticle(from url: String) async throws !" String { guard let url = URL(string: url) else { throw AnalyserError.badURL } return try await withUnsafeThrowingContinuation { continuation in URLSession.shared.downloadTask(with: url) { (localUrl, urlResponse, error) in guard let localUrl = localUrl else { continuation.resume(throwing: AnalyserError.badURL) return } if let htmlText = try? String(contentsOf: localUrl) { continuation.resume(returning: htmlText) } } .resume() } } Solution: Use async/await Swift 5.5

Slide 132

Slide 132 text

func fetchArticle(from url: String) async throws !" String { guard let url = URL(string: url) else { throw AnalyserError.badURL } return try await withUnsafeThrowingContinuation { continuation in URLSession.shared.downloadTask(with: url) { (localUrl, urlResponse, error) in guard let localUrl = localUrl else { continuation.resume(throwing: AnalyserError.badURL) return } if let htmlText = try? String(contentsOf: localUrl) { continuation.resume(returning: htmlText) } } .resume() } } Solution: Use async/await Swift 5.5

Slide 133

Slide 133 text

auth!$signInAnonymously { result, error in guard let result = result else { return } print("User signed in with user ID: \(result.user.uid)") } do { let result = try await Auth.auth().signIn(withEmail: email, password: password) print("User signed in with user ID: \(result.user.uid)") } catch { print(error) } Works with Firebase, too! Callback-style

Slide 134

Slide 134 text

Learn more h"ps://pete#riese.dev/async-await-in-swi! h"ps://www.youtube.com/watch?v=sEKw2BMcQtQ

Slide 135

Slide 135 text

Architecture Swi!UI 2 Life Cycle Firebase Firestore Authentication Combine async/await Completion block WidgetKit

Slide 136

Slide 136 text

Architecture Swi!UI 2 Life Cycle Firebase Firestore Authentication Combine async/await Completion block WidgetKit

Slide 137

Slide 137 text

Register now - it’s free! h"ps://io.google/2022/ Google I/O 2022

Slide 138

Slide 138 text

Thanks! @pete#riese h"ps://github.com/pete#riese/BookShelf Code @charlo"eCLiang

Slide 139

Slide 139 text

Credits %nish by Megan Chown from the Noun Project Time by Nikita Kozin from the Noun Project pipe by Komkrit Noenpoempisut from the Noun Project Passpo& by ProSymbols from the Noun Project spiral by Alexander Skowalsky from the Noun Project Architecture by Ervin Bolat from the Noun Project Firebase logos cou&esy h"ps://%rebase.google.com/brand-guidelines Firebase logos cou&esy h"ps://%rebase.google.com/brand-guidelines Thanks! Hea& by Roman from the Noun Project Dashboard by BomSymbols from the Noun Project

Slide 140

Slide 140 text

The End.