View doesn't match ObservedObject - asynchronous

I am using an ObservedObject with a singleton which is populated via an API call to present part of my view. However, when I update my singleton, the view isn't updated accordingly. Here is my view code:
struct Friends: View {
#State private var friends: [FirestoreUser] = []
#State private var loading = false
#ObservedObject private var currentUser = CurrentUser.shared // singleton
// Bunch of UI code ...
GeometryReader { subGeo in
VStack {
List {
ForEach(friends, id: \.id) {
UserItem(user: $0)
.background(Color.white)
}
.listRowBackground(Color.white)
}
.listRowBackground(Color.white)
Spacer()
// Some brackets and then ...
.onReceive(self.currentUser.$user, perform: { _ in
getFriends()
})
Here's the getFriends() function:
func getFriends() {
print("in get friends func")
if let friendUIDs = currentUser.user?.firestoreFriendUIDs {
print("getting friends for friends page")
if friendUIDs.count > 0 {
self.loading = true
FirestoreService.fetchUsers(uids: friendUIDs) { (friends) in
self.friends = friends!
self.loading = false
}
}
}
}
Here is my CurrentUser class:
import Foundation
import SwiftUI
class CurrentUser: ObservableObject {
#Published var user: FirestoreUser?
public static var shared = CurrentUser()
func setUser(user: FirestoreUser) {
self.user = user
}
}
And here is where I'm initially setting the singleton:
func listen() {
handle = auth.addStateDidChangeListener({ (auth, user) in
if let authUser = user {
print("user in addStateDidChangeListener is not nil")
self.loggedInFIRAuthUser = user
FirestoreService.fetchUser(uid: authUser.uid) { (user) in
DispatchQueue.main.async {
if let user = user {
CurrentUser.shared.setUser(user: user)
print("set CurrentUser singleton")
}
}
}
} else {
print("nil authUser")
}
})
}
This is part of the initial view for the app. What's happening based on print messages is that the program fetches user data from Firestore when it starts up, confirms the user has friends, but onReceived is called just before the singleton is set rather than after. I even tried adding a delay, and the singleton is actually waiting for the .OnReceived code to run. The order of the printed lines is:
in get friends func
user in addStateDidChangeListener is not nil
friendUIDs.count: 1
in get friends func
set CurrentUser singleton
Any help is greatly appreciated, I've been stuck on this for days.

Related

Swiftui Force Update View

In my content view I have a home page with some text that says "Welcome, xxxx" where xxxx is the name fetched from a firebase database. This field can be changed in the settings page that is navigated to via a Navigation Link. When the name is changed and saved the name on the home page only updates when you force shutdown the app. How do I force update the view when you press the back button from settings.
This is how I display the field:
Text("Welcome, \(companyName)")
.font(.system(size: 23))
.bold()
.foregroundColor(Color("background"))
.padding(.bottom, 50)
This is how I set a value to companyName:
func SetData() {
var db = Firestore.firestore()
let user = Auth.auth().currentUser
let userName = user?.email ?? ""
let docRef = db.collection("CONTACT").document(userName)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
//Setting Values
let data = document.data()
self.companyName = data?["companyName"] as? String ?? ""
} else {
print("Document does not exist")
}
}
}
There are several solutions to this, but you haven't provided enough code outlining what you have done to modify the variable companyName. The easiest solution would be to pass companyName as a binding value into the settings.
What I imagine here is that your HomeView is fetching the data on launch. In the settings, a change data request is made, but nothing is done to update the data in the HomeView. By using a binding variable we can ensure that the companyName connects to the source of truth in the HomeView, and so the function modifies the companyName which is precisely the company name on the HomeView vs. modifying potentially the value of companyName.
struct HomeView: View {
#State var companyName = "Microsoft"
var body: some View {
NavigationView {
NavigationLink(destination: SettingsView(companyName: $companyName)) {
Text("Tap to navigate to Settings")
}
}
}
}
struct SettingsView: View {
#Binding var companyName : String
var body: some View {
Button {
SetData()
} label: {
HStack {
Text("Tap to change!")
Text("\(companyName)!")
}
}
}
func SetData() {
var db = Firestore.firestore()
let user = Auth.auth().currentUser
let userName = user?.email ?? ""
let docRef = db.collection("CONTACT").document(userName)
docRef.getDocument { (document, error) in
if let document = document, document.exists {
//Setting Values
let data = document.data()
self.companyName = data?["companyName"] as? String ?? ""
} else {
print("Document does not exist")
}
}
}
}
If you have already done this at it doesn't somehow work, another solution is to add an .onAppear modifier to your HomeView.
struct HomeView: View {
#State var companyName = "Microsoft"
var body: some View {
VStack {
// code ...
}
.onAppear {
fetchData()
}
}
func fetchData() {
// code that returns companyFetchedName
self.companyName = companyFetchedName
}
}
Modify it on main queue, like
docRef.getDocument { (document, error) in
if let document = document, document.exists {
//Setting Values
let data = document.data()
DispatchQueue.main.async { // << here !!
self.companyName = data?["companyName"] as? String ?? ""
}
} else {
print("Document does not exist")
}
}

Access Data from Firestore and Put it in SwiftUI EnvironmentObject

I am trying to use Firestore and get the data from the Firestore and then put it in EnvironmentObject. But it is not working out. I tried different approaches each ended with different errors.
Here is my code:
class FirebaseManager: ObservableObject {
#Published var tweets: [Tweet] = []
private let db: Firestore = Firestore.firestore()
init() {
db.collection("tweets")
.addSnapshotListener { snapshot, error in
if let error {
print(error.localizedDescription)
return
}
let tweets = snapshot?.documents.compactMap({ document in
try? document.data(as: Tweet.self)
})
if let tweets {
DispatchQueue.main.async {
self.tweets = tweets
}
}
}
}
}
Then I try to call FirebaseManager in the code below:
struct HomeTimelineScreen: View {
#EnvironmentObject var appState: AppState
#StateObject private var firebaseManager = FirebaseManager()
private var cancellables = Set<AnyCancellable>()
init() {
// ERROR Escaping closure captures mutating 'self' parameter
firebaseManager.$tweets.sink { tweets in
appState.tweets = tweets
}
// ERROR/WARNING ObservableObject of type AppState found. A View.environmentObject(_:) for AppState may be missing as an ancestor of this view.
firebaseManager.$tweets.assign(to: \.appState.tweets, on: self)
.store(in: &cancellables)
}
The EnvironmentObject AppState is injected in the TwitterApp main file as shown below:
#main
struct TwitterAppApp: App {
#ObservedObject var coordinator = Coordinator()
init() {
FirebaseApp.configure()
}
var body: some Scene {
WindowGroup {
NavigationStack(path: $coordinator.path) {
LandingScreen()
.navigationDestination(for: Route.self) { route in
switch route {
case .login:
LoginScreen().appLogoToolbar()
case .register:
RegistrationScreen().appLogoToolbar()
case .home:
HomeScreen()
case .detail(let tweet):
TweetDetailsScreen(tweet: tweet)
}
}
}.environmentObject(coordinator)
.environmentObject(AppState())
}
}
}
AppState:
import Foundation
class AppState: ObservableObject {
#Published var tweets: [Tweet] = []
}
UPDATE: My biggest issue is on how to re-render the TweetDetailScreen since it is also using the TweetCellView.
struct TweetDetailsScreen: View {
let tweet: Tweet
var body: some View {
List {
TweetCellView(tweet: tweet)
ForEach(1...20, id: \.self) { index in
Text("\(index)")
}
}
}
}
I took your advice and added single source of truth FirebaseManager and now it works as I want it to be.
struct TweetDetailsScreen: View {
#EnvironmentObject var firebaseManager: FirebaseManager
let tweet: Tweet
var body: some View {
List {
if let tweet = firebaseManager.findByDocumentId(tweet.documentID ?? "") {
TweetCellView(tweet: tweet)
}
ForEach(1...20, id: \.self) { index in
Text("\(index)")
}
}
}
}
class FirebaseManager: ObservableObject {
#Published var tweets: [Tweet] = []
private let db: Firestore = Firestore.firestore()
init() {
db.collection("tweets")
.addSnapshotListener { snapshot, error in
if let error {
print(error.localizedDescription)
return
}
let tweets = snapshot?.documents.compactMap({ document in
try? document.data(as: Tweet.self)
})
if let tweets {
DispatchQueue.main.async {
self.tweets = tweets
}
}
}
}
func findByDocumentId(_ documentId: String) -> Tweet? {
guard let index = tweets.firstIndex(where: { $0.documentID == documentId }) else { return nil }
return tweets[index]
}
}

Updates To Firebase Firestore Document Popping View Off Navigation Stack SwiftUI

I am working on a SwiftUI app and using Firebase Firestore as a backend. I am noticing an odd behavior when I update a document in Firestore that results in a view being popped off the Navigation stack each time a field is updated. The Navigation Stack is as follows.
MarketplaceView with List Containing ListingRowViews
Tapping a row takes you to a ListingDetailView.
Tapping a Buy Now button in ListingDetailView takes you to a ConfirmationView.
When updates are made to the document and 1 or 2 is present, the view updates and all behaves as expected. When 3 (ConfirmationView) is present, any updates to the document result in the ConfirmationView being popped off the stack back to 2 the ListingDetailView. Below is the app architecture.
ListingRepository - Creates a snapshotListener for all Listings in Firestore.
class ListingRepository: ObservableObject {
let db = Firestore.firestore()
private var snapshotListener: ListenerRegistration?
#Published var listings = [Listing]()
private var cancellables = Set<AnyCancellable>()
init() {
startSnapshotListener()
}
func startSnapshotListener() {
// Add a SnapshotListener to the Listing Collection.
self.snapshotListener = db.collection(FirestoreCollection.listings).addSnapshotListener { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
// Check to make sure the Collection contains Documents
guard let documents = querySnapshot?.documents else {
print("No Listings.")
return
}
// Documents exist.
self.listings = documents.compactMap { listing in
do {
return try listing.data(as: Listing.self)
} catch {
print(error)
}
return nil
}
}
}
}
}
MarketplaceViewModel - Subscribes to Listings from ListingRepository and creates ListingRowViewModels.
class MarketplaceViewModel: ObservableObject {
// Properties
#Published var listingRepository: ListingRepository = Resolver.resolve()
// Published Properties
#Published var listingRowViewModels = [ListingRowViewModel]()
// Combine Cancellable
private var cancellables = Set<AnyCancellable>()
// Intitalizer
init() {
self.startCombine()
}
// Starting Combine
func startCombine() {
listingRepository
.$listings
.receive(on: RunLoop.main)
.map { listings in
listings
.map { listing in
ListingRowViewModel(listing: listing)
}
}
.assign(to: \.listingRowViewModels, on: self)
.store(in: &cancellables)
}
}
MarketplaceView - Creates the List full of ListingDetailViews
struct MarketplaceView: View {
#ObservedObject var marketplaceViewModel: MarketplaceViewModel = Resolver.resolve()
var body: some View {
return NavigationView {
List {
ForEach(self.marketplaceViewModel.listingRowViewModels, id: \.id) { listingRowViewModel in
NavigationLink(destination: ListingDetailView(listingDetailViewModel: ListingDetailViewModel(listing: listingRowViewModel.listing))
) {
ListingRowView(listingRowViewModel: listingRowViewModel)
}
} // ForEach
.navigationTitle("Marketplace")
} // NavigationView
} // View
}
}
ListingRowViewModel - View model for each row.
class ListingRowViewModel: ObservableObject, Identifiable {
// Properties
var id: String = ""
// Published Properties
#Published var listing: Listing
// Combine Cancellable
private var cancellables = Set<AnyCancellable>()
// Initializer
init(listing: Listing) {
self.listing = listing
self.startCombine()
}
// Starting Combine
func startCombine() {
$listing
.receive(on: RunLoop.main)
.compactMap { listing in
listing.id
}
.assign(to: \.id, on: self)
.store(in: &cancellables)
}
}
ListingRowView - View for each row.
struct ListingRowView: View {
#ObservedObject var listingRowViewModel: ListingRowViewModel
var body: some View {
Text(self.listingRowViewModel.listingId)
} // View
}
ListingDetailViewModel - View model for the detail view.
class ListingDetailViewModel: ObservableObject, Identifiable {
var listing: Listing
// Initializer
init(listing: Listing) {
self.listing = listing
}
}
ListingDetailView - Detail View For Listing
struct ListingDetailView: View {
var listingDetailViewModel: ListingDetailViewModel
var body: some View {
VStack {
Text(self.listingDetailViewModel.listing.id)
NavigationLink(destination: ConfirmationView(confirmationViewModel: ConfirmationViewModel(listing: listing))) {
Text("Buy Now")
}
}
.navigationTitle("Listing Info")
} // View
}
ConfirmationViewModel - View model for Confirmation View
class ConfirmationlViewModel: ObservableObject, Identifiable {
var listing: Listing
// Initializer
init(listing: Listing) {
self.listing = listing
}
}
ConfirmationView - Confirmation View
struct ConfirmationView: View {
var confirmationViewModel: ConfirmationViewModel
var body: some View {
VStack {
Text(self.confirmationViewModel.listing.id)
Button(action: {
self.confirm()
}, label: {
Text("Confirm")})
}
.navigationTitle("Order Confirmation")
} // View
}
Any help would be greatly appreciated.

Repeated messages in chatView. how to clear view?

I have a chatView with a list of chatRow Views (messages)
each chatView has a snapshot listener with firebase, so I should get real time updates if I add a new message to the conversation
The problem I have is: when I add a new message my chatView shows ALL the messages I added before plus the new message, PLUS the same list again....if I add another message then the list repeats again
I assume I need to drop/refresh the previous views shown in the Foreach loop...how can I drop/refresh the view so it can receive refreshed NON repeated data?
struct ChatView: View {
#EnvironmentObject var chatModel: ChatsViewModel
let chat: Conversation
let user = UserService.shared.user
#State var messagesSnapshot = [Message]()
#State var newMessageInput = ""
var body: some View {
NavigationView {
VStack {
ScrollViewReader { scrollView in
ScrollView {
ForEach(chat.messages, id: \.id) { message in
if user.name == message.createdBy {
ChatRow(message: message, isMe: true)
} else {
ChatRow(message: message, isMe: false)
}
}
.onAppear(perform: {scrollView.scrollTo(chat.messages.count-1)})
}
}
Spacer()
//send a new message
ZStack {
Rectangle()
.foregroundColor(.white)
RoundedRectangle(cornerRadius: 20)
.stroke(Color("LightGrayColor"), lineWidth: 2)
.padding()
HStack {
TextField("New message...", text: $newMessageInput, onCommit: {
print("Send Message")
})
.padding(30)
Button(action: {
chatModel.sendMessageChat(newMessageInput, in: chat, chatid: chat.id ?? "")
print("Send message.")
}) {
Image(systemName: "paperplane")
.imageScale(.large)
.padding(30)
}
}
}
.frame(height: 70)
}
.navigationTitle("Chat")
}
}
}
function to add message to the conversation
func addMessagesToConv(conversation: Conversation, index: Int) {
var mensajesTotal = [Message]()
let ref = self.db.collection("conversations").document(conversation.id!).collection("messages")
.order(by: "date")
.addSnapshotListener { querySnapshotmsg, error in
if error == nil {
//loop throug the messages/docs
for msgDoc in querySnapshotmsg!.documents {
var m = Message() //emtpy struc message
m.createdBy = msgDoc["created_by"] as? String ?? ""
m.date = msgDoc["date"] as? Timestamp ?? Timestamp()
m.msg = msgDoc["msg"] as? String ?? ""
m.id = msgDoc.documentID //firebase auto id
mensajesTotal.append(m) //append this message to the total of messages
self.chats[index].messages.removeAll()
self.chats[index].messages = mensajesTotal
}
} else {
print("error: \(error!.localizedDescription)")
}
}
}
You've defined mensajesTotal outside of your snapshot listener. So, it's getting appended to every time.
To fix this, move this line:
var mensajesTotal = [Message]()
to inside the addSnapshotListener closure.
You have two options:
Clear mensajesTotal each time you get an update from the database, as #jnpdx's answer shows.
Process the more granular updates in querySnapshotmsg.documentChanges to perform increment updates in your UI, as also shown in the documentation on detecting changes between snapshots.
There is no difference in the data transferred between client and server between these approaches, so use whatever is easiest (that'd typically be #1) or most efficient on the UI (that's usually #2).

SwiftUI - Simple Firestore Query - Display Results

Ok, I'll preface this with the fact that I'm new to SwiftUI and programming. I'm a UX Designer. However, I'm trying to run a simple Firestore query and return the results to a list.
I've been able to write a function that writes the results to the console successfully, but I have no idea how to access the information that's within the function so that I can use it within the main view of the page.
I've started a simple view so that I can just focus on displaying Firestore data in a list. Here's my barebones code currently.
import SwiftUI
import FirebaseFirestore
struct FestivalListFB: View {
let db = Firestore.firestore()
func getVenues() {
let db = Firestore.firestore()
db.collectionGroup("Venues").getDocuments() {(querySnapshot, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
for document in querySnapshot!.documents {
guard let venueEntry = document.get("venueTitle") as? String else {
continue
}
print(venueEntry)
}
}
}
}
var body: some View {
VStack {
List(0 ..<5) { item in
Text("Hello, World!")
}
}.onAppear(perform: getVenues)
}
}
Console displays:
"Refreshment Outpost
Holiday Sweets & Treats
Fife & Drum Tavern
L'Artisan des Glaces
Shi Wasu
...etc"
And of course, the body only displays "Hello World" 5 times within a list. How would I go about accessing the values in "venueEntry" so that I can display it within the list element?
I've included a image of my Firestore data structure as well. Ideally, I'd like to display the venues grouped by the "venueArea" they are located in.
For easier use, i created a model for you venue. See the below snippet how you can show your data in your View.
Your model:
class VenueObject: ObservableObject {
#Published var venueID: String
#Published var venueTitle: String
#Published var venueArea: String
init(id: String, title: String, area: String) {
venueID = id
venueTitle = title
venueArea = area
}
}
Your View:
struct FestivalListFB: View {
#State var data: [VenueObject] = []
let db = Firestore.firestore()
var body: some View {
VStack {
ForEach((self.data), id: \.self.venueID) { item in
Text("\(item.venueTitle)")
}
}.onAppear {
self.getVenues()
}
}
func getVenues() {
// Remove previously data to prevent duplicate data
self.data.removeAll()
self.db.collectionGroup("Venues").getDocuments() {(querySnapshot, err) in
if let err = err {
print("Error getting documents \(err)")
} else {
for document in querySnapshot!.documents {
let id = document.documentID
let title = document.get("venueTitle") as! String
let area = document.get("venueArea") as! String
self.data.append(VenueObject(id: id, title: title, area: area))
}
}
}
}
}

Resources