iTranslated by AI
SwiftUI + Firestore: Triggering Navigation and Data Transfer on Document Add/Update
What I want to achieve
- Keep a standby screen displayed during normal times, and transition to a new screen if a Firestore document is added or updated "while waiting."
- Display the contents of the added/updated document on the destination screen.
- Change a field value when the "Confirm" button is pressed on the destination screen.
This might be useful for creating UIs triggered by real-time state changes, such as calling ticket numbers, real-time order processing, or call buttons.
Tools
Using addSnapshotListener allows for continuous asynchronous monitoring of Firestore.
Inside the function triggered by update detection, we will perform data formatting and assignment, and then use .navigationDestination to transition screens by observing variable changes.
When updating the document after the transition, we pass the document ID in advance and update it using .updateData.
Implementation
First, we will build only the part that detects updates, and then we will create the process to pass the updated data to the destination screen.
Once the operation is confirmed, we will add the process to pass the document contents to the destination screen.
Implementing only update detection and screen transition for now
Don't forget to add FirebaseCore and FirebaseFirestore to your dependencies.

Write the code while referring to the Firebase documentation.
First, let's simply create a feature that automatically transitions the screen when an update is detected.
import Combine
import FirebaseCore
import FirebaseFirestore
class FirestoreListener: ObservableObject{
// Screen transition flag
// We want this change to trigger a screen transition, so use @Published
@Published var flagTransition: Bool = false
// Initialization
private var db = Firestore.firestore()
private var listener: ListenerRegistration?
// Flag to prevent the initial screen transition
private var isFirstFetch: Bool = true
// Start listening
func startListening(){
listener = db.collection("collection1") // Specify the collection to monitor
.whereField("status",isEqualTo: "hoge") // Monitor only those where status is hoge
.addSnapshotListener {querySnapshot, error in
// Stop processing if an error occurs (if error is not nil)
if let error = error {
print("An error occurred: \(error.localizedDescription)")
return
}
// querySnapshot can be nil due to permission errors or communication errors
// It is not nil when there are 0 results from whereField
guard let snapshot = querySnapshot else{
print("snapshot is empty")
return
}
// addSnapshotListener fetches current data once initially and fires, so end without transition on the first load
if self.isFirstFetch {
self.isFirstFetch = false
return
}
// Check each changed document one by one
for change in snapshot.documentChanges {
// If a document was added or modified
if change.type == .added || change.type == .modified {
print("Data with status == hoge was added or changed")
// Changes to transition flags must be done on the main thread (Swift convention)
DispatchQueue.main.async {
self.flagTransition = true
}
}
}
}
}
// Stop listening and return to initial state
func stopListening(){
listener?.remove()
listener = nil
isFirstFetch = true
}
}
import SwiftUI
struct ContentView: View {
// The class we just created to hold the Firestore monitoring state
// Using @StateObject so it isn't re-initialized on every redraw
@StateObject private var firestoreListener = FirestoreListener()
var body: some View {
NavigationStack{
VStack(spacing: 20){
ProgressView()
.controlSize(.large)
Text("Monitoring Firestore...")
.font(.headline)
}
.navigationTitle("Home")
// Transition to SuccessView when flagTransition becomes true
.navigationDestination(isPresented: $firestoreListener.flagTransition){
SuccessView()
}
// Start listening as soon as the screen appears
.onAppear{
firestoreListener.startListening()
}
// Stop listening when switching to another screen
.onDisappear{
firestoreListener.stopListening()
}
}
}
}
// Destination screen (can be in a separate file)
struct SuccessView: View {
var body: some View {
VStack{
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 60))
.symbolEffect(.breathe)
.foregroundStyle(.green)
Text("Update Detected")
.font(.title)
.padding()
}
.navigationTitle("Success")
}
}
// For preview
#Preview {
ContentView()
}
Quick check of operations
- Display the preview on Xcode (or run it in a simulator) and keep the "Monitoring Firestore..." screen visible.
- In the Firebase console in your browser, add a new document to
collection1and set the fieldstatus : "hoge"and save it.
- If the screen changes to "Update Detected", it's successful.
Using document contents on the destination screen
Now for the main topic. As stated in the heading, we want to display the fields of the updated document on the destination screen.
For now, we will display the contents of the text field.
At this point, we are already capturing documents where the status field changed to hoge, so we just need to add the process to extract the document contents and put them into a variable.
import Combine
import FirebaseCore
import FirebaseFirestore
+// Create a struct to bundle the data you want to pass to the destination
+// We'll use this as a transition trigger, so use Identifiable and Hashable
+struct TransitionData: Identifiable, Hashable{
+ let id = UUID() // Randomly generated unique ID. Required for Identifiable
+ let text: String // Variable to hold the value of the text field
+}
class FirestoreListener: ObservableObject{
+ // Trigger screen transition when this variable is no longer nil (data is added)
+ @Published var transitionData: TransitionData?
- // Screen transition flag
- // We want this change to trigger a screen transition, so use @Published
- @Published var flagTransition: Bool = false
// Initialization
private var db = Firestore.firestore()
private var listener: ListenerRegistration?
// Flag to prevent the initial screen transition
private var isFirstFetch: Bool = true
// Start listening
func startListening(){
listener = db.collection("collection1") // Specify the collection to monitor
.whereField("status",isEqualTo: "hoge") // Monitor only those where status is hoge
.addSnapshotListener {querySnapshot, error in
// Stop processing if an error occurs
if let error = error {
print("An error occurred: \(error.localizedDescription)")
return
}
// querySnapshot can be nil due to permission errors or communication errors. It is not nil when there are 0 results from whereField
guard let snapshot = querySnapshot else{
print("snapshot is empty")
return
}
// addSnapshotListener fetches current data once initially and fires, so end without transition on the first load
if self.isFirstFetch {
self.isFirstFetch = false
return
}
// Check each changed document one by one
for change in snapshot.documentChanges {
// If a document was added or modified
if change.type == .added || change.type == .modified {
+ // Extract (all) the contents of the document
+ let documentData = change.document.data()
+ // Extract value for key "text" as String; otherwise default to "no text"
+ let fetchedText = documentData["text"] as? String ?? "no text"
+ print("Data added or changed. Fetched text: \(fetchedText)")
- print("Data with status == pending was added or changed")
+ // Put the extracted text into the variable on the main thread
DispatchQueue.main.async {
+ self.transitionData = TransitionData(text: fetchedText)
- self.flagTransition = true
}
}
}
}
}
// Stop listening and return to initial state
func stopListening(){
listener?.remove()
listener = nil
isFirstFetch = true
}
}
Previously, we used a flag for screen transition, but this time we will rewrite it to trigger the transition when the variable is no longer nil.
import SwiftUI
struct ContentView: View {
// The class we just created to hold the Firestore monitoring state
@StateObject private var firestoreListener = FirestoreListener()
var body: some View {
NavigationStack{
VStack(spacing: 20){
ProgressView()
.controlSize(.large)
Text("Monitoring Firestore...")
.font(.headline)
}
.navigationTitle("Home")
+ // Transition to SuccessView when data enters transitionData
+ .navigationDestination(item: $firestoreListener.transitionData){data in
+ SuccessView(receivedText: data.text)
+ }
- // Transition to SuccessView when flagTransition becomes true
- .navigationDestination(isPresented: $firestoreListener.flagTransition){
- SuccessView()
- }
.onAppear{
// Start listening as soon as the screen appears
firestoreListener.startListening()
}
.onDisappear{
// Stop listening when switching to another screen
firestoreListener.stopListening()
}
}
}
}
// Destination screen (can be in a separate file)
struct SuccessView: View {
+ // Variable to receive the text
+ let receivedText: String
var body: some View {
VStack{
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 60))
.symbolEffect(.breathe)
.foregroundStyle(.green)
Text("Update Detected")
.font(.title)
.padding()
+ // Display the received text
+ Text(receivedText)
+ .font(.title)
+ .bold()
+ .padding()
+ .background(Color.gray.opacity(0.1))
+ .cornerRadius(10)
}
.navigationTitle("Success")
}
}
// For preview
#Preview {
ContentView()
}
Quick check of operations
- Display the preview on Xcode (or run it in a simulator) and keep the "Monitoring Firestore..." screen visible.
- In the Firebase console in your browser, add a new document to
collection1and set the fieldsstatus : "hoge"andtext : fuga, then save it. - If the screen changes to "Update Detected" and
fugais displayed, it's successful.
Updating Firestore data when a button is pressed on the destination screen
We will add a "Confirm" button to the destination screen, and when pressed, we will add a process to change the Firestore status field from hoge to completed.
To identify the document that needs to be updated at the destination, we will add a process to pass the document ID to the destination screen as well.
As a bonus, since document IDs are unique, we will use them instead of a UUID.
struct TransitionData: Identifiable, Hashable {
- let id = UUID()
+ let id: String
let text: String
}
for change in snapshot.documentChanges {
if change.type == .added || change.type == .modified {
let documentData = change.document.data()
let fetchedText = documentData["text"] as? String ?? "no text"
print("Data added or changed. Fetched text: \(fetchedText)")
+ // Get the document ID
+ let docId = change.document.documentID
DispatchQueue.main.async {
+ // Put the document ID along with the text into the variable
+ self.transitionData = TransitionData(id: docId, text: fetchedText)
- self.transitionData = TransitionData(text: fetchedText)
}
}
}
Add the logic to the View to manipulate Firestore as well, changing the contents of the document specified by the document ID when the button is pressed.
import SwiftUI
+import FirebaseFirestore
struct ContentView: View {
// The class we just created to hold the Firestore monitoring state
@StateObject private var firestoreListener = FirestoreListener()
var body: some View {
NavigationStack{
VStack(spacing: 20){
ProgressView()
.controlSize(.large)
Text("Monitoring Firestore...")
.font(.headline)
}
.navigationTitle("Home")
// Transition to SuccessView when data enters transitionData
.navigationDestination(item: $firestoreListener.transitionData){data in
+ // Pass the whole thing
+ SuccessView(receivedData: data)
- SuccessView(receivedText: data.text)
}
.onAppear{
// Start listening as soon as the screen appears
firestoreListener.startListening()
}
.onDisappear{
// Stop listening when switching to another screen
firestoreListener.stopListening()
}
}
}
}
// Destination screen (can be in a separate file)
struct SuccessView: View {
+ // Receive everything
+ let receivedData: TransitionData
- let receivedText: String
// Automatically return to the previous screen when the button is pressed
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack{
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 60))
.symbolEffect(.breathe)
.foregroundStyle(.green)
Text("Update Detected")
.font(.title)
.padding()
// Extract and display the text from the received data
+ Text(receivedData.text)
- Text(receivedText)
.font(.title)
.bold()
.padding()
.background(Color.gray.opacity(0.1))
.cornerRadius(10)
}
.navigationTitle("Success")
+ // Confirm button
+ Button(action: {
+ // Execute function when button is pressed
+ updateStatusToCompleted()
+ }){
+ Text("Confirm")
+ .font(.headline)
+ .foregroundStyle(.white)
+ .padding()
+ .frame(maxWidth: .infinity)
+ .background(Color.blue)
+ .cornerRadius(10)
+ }
}
+ // Function to change the document status field to completed
+ private func updateStatusToCompleted(){
+ // Action when button is pressed
+ let db = Firestore.firestore()
+ db.collection("collection1") // Specify the monitored collection
+ .document(receivedData.id) // Specify the update target by document ID
+ .updateData([
+ "status": "completed" // Update status
+ ]){error in
+ if let error = error{
+ print("Update error: \(error.localizedDescription)")
+ } else {
+ print("Status updated → completed")
+ // Return to the previous screen after a successful update
+ dismiss()
+ }
+ }
+ }
}
// For preview
#Preview {
ContentView()
}
Quick check of operations
- Display the preview on Xcode (or run it in a simulator) and keep the "Monitoring Firestore..." screen visible.
- In the Firebase console in your browser, add a new document to
collection1and set the fieldsstatus : "hoge"andtext : fuga, then save it. - The screen changes to "Update Detected" and
fugais displayed. - It is successful if you can confirm on the Firebase console that the field has been modified after pressing the "Confirm" button.
Closing
There are many points to correct when using this in a real-world application, but I'm leaving it here as a note for now.
Discussion