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.
5. 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)
11. 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
30. struct Todo {
@DocumentID var docId: String?
var id: String? = UUID().uuidString
var title: String
var completed: Bool = false
var userId: String?
}
Data Model
45. 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
46. 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
55. 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
56. 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
57. 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
58. 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
59. 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
60. 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
61. All todos are stored in one single collection
Which user do
they belong to?
62. let query = db.collection(“todos")
.whereField("userId",
isEqualTo: self.userId)
query
.addSnapshotListener { [weak self] (querySnapsho
guard let documents = querySnapshot!$documents els
Signed in user
64. 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
65. 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!
66. 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
68. 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
73. 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
74. 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")
}
75. 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!
76. 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!
77. 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")
}
78. 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.")")
}
}
}
79. 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.")")
}
}
}
80. 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.")")
}
}
}
81. 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
}
}
82. 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
}
}
83. 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
}
}
85. 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