In this talk, I am going to walk you through the building blocks for creating modern applications on Apple’s platforms using SwiftUI and Firebase.
We will cover the following topics:
- Setting up an architecture for data-driven applications
- What’s new in SwiftUI’s application life cycle
- Syncing data in realtime across multiple devices with Cloud Firestore
- Mapping Firestore documents in a type-safe way using the Codable protocol
- Using Combine to fetch data from remote APIs and Firebase
- How async/await (new in Swift 5.5) will affect the way we write asynchronous code (plus some surprises in Firebase's code base)
Firebase is always evolving, so I will also talk about how we’re making use of Apple’s latest technologies in our own code base, for example
- Combine
- async/await
- SwiftUI view modifiers
No matter if you’re a seasoned Firebase user or just wondering what it is all about, you should leave this session with a deeper understanding of what Firebase it and how you can use it your apps on Apple’s platforms.
31. 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")
}
}
}
}
32. 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")
}
}
}
}
33. 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")
}
34. 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()
}
49. struct Book: Identifiable {
var id = UUID().uuidString
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
50. 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
51. 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
52. 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
53. 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
54. 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
55. 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
56. 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
Can we do better?
58. 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
59. 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)
}
}
}
}
Mapping data using Codable
if let document = document {
do {
self.book = try document.data(as: Book.self)
}
catch {
print(error)
}
}
60. 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)
}
}
}
}
Mapping data using Codable
if let document = document {
do {
self.book = try document.data(as: Book.self)
}
catch {
print(error)
}
}
63. 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
64. 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
65. 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
66. 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
67. 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
68. 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
69. 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
81. 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) { !!& }
self.presentationMode.wrappedValue.dismiss()
}
}
).frame(width: 280, height: 45, alignment: .center)
Sign in with Apple
82. 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) { !!& }
self.presentationMode.wrappedValue.dismiss()
}
}
).frame(width: 280, height: 45, alignment: .center)
Sign in with Apple
83. 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) { !!& }
self.presentationMode.wrappedValue.dismiss()
}
}
).frame(width: 280, height: 45, alignment: .center)
Sign in with Apple
84. 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) { !!& }
self.presentationMode.wrappedValue.dismiss()
}
}
).frame(width: 280, height: 45, alignment: .center)
Sign in with Apple
85. 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) { !!& }
self.presentationMode.wrappedValue.dismiss()
}
}
).frame(width: 280, height: 45, alignment: .center)
Sign in with Apple
86. 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) { !!& }
self.presentationMode.wrappedValue.dismiss()
}
}
).frame(width: 280, height: 45, alignment: .center)
Sign in with Apple
87. All books are stored in one single collection
Which user do
they belong to?
88. let query = db.collection("books")
.whereField("userId",
isEqualTo: self.userId)
query
.addSnapshotListener { [weak self] (querySnapsho
guard let documents = querySnapshot!$documents els
Signed in user
89. 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
93. 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
94. 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
95. Follow the project for updates
h"ps://github.com/#rebase/#rebase-ios-sdk/projects/3
98. 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
99. 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
Probably in Swift 5.5
100. 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
Probably in Swift 5.5
101. 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
Probably in Swift 5.5
102. 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
Probably in Swift 5.5
103. 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
Probably in Swift 5.5
104. 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
108. 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