How to refresh view with fetched data - Firestore & SwiftUI - firebase

Short: The Images in my view are not updating after the first load. The URL remains the same as the previous loaded view, however the rest of the view that doesn't fetch a URL or data from storage is updated.
Full: I have two Views, a ListView and a DetailView.
In the ListView I display a list of type List. The detail view is supposed to show each Profile from List.profiles. I do this by storing each string uid in List.profiles and calling model.fetchProfiles to fetch the profiles for each list selected.
On the first selected List model.fetchProfiles returns the documents and model.profiles displays the data fine in the DetailView.
When first loading the DetailView the ProfileRow on appear is called and logs the profiles fetched. Then the ProfileRow loads the imageURL from the imagePath and uses it like to fetch the image.
Console: Load List1
CARD DID APPEAR: Profiles []
CARD DID APPEAR: SortedProfiles []
CARD ROW
CARD ROW DID APPEAR: Profiles profiles/XXXXXX/Profile/profile.png
CARD ROW DID APPEAR: SortedProfiles profiles/XXXXXX/Profile/profile.png
Get url from image path: profiles/XXXXXX/Profile/profile.png
Image URL: https://firebasestorage.googleapis.com/APPNAME/profiles%XXXXXXX
When selecting the second List from ListView the ProfileRow didAppear is not called due to;
if model.profiles.count > 0 {
print("CARD ROW DID APPEAR: Profiles \(model.profiles[0]. imgPath)")
print("CARD ROW DID APPEAR: Sorted \(model.sortedProfiles[0].imgPath)")
}
and won't ever again when selecting a List in ListView, however the rest of the profile data in the ProfileRow is displayed such as name so the data must be fetched.
The ImagePath is the same as the first view loading the exact same image. All other properties for the Profile such as name are loaded correctly.
Console: Load List2
CARD DID APPEAR: Profiles []
CARD DID APPEAR: SortedProfiles []
CARD ROW
Get url from image path: profiles/XXXXXX/Profile/profile.png
Image URL:
https://firebasestorage.googleapis.com/APPNAME/profiles%XXXXXXX
If I then navigate to List1 then the image for List2 appears, if I reselect List2 the image appears fine. The image show is correct on first load, and when selecting another list it always the one from before.
Can anyone help me out ?
First View
struct ListViw: View {
#EnvironmentObject var model: Model
var body: some View {
VStack {
ForEach(model.lists.indices, id: \.self) { index in
NavigationLink(
destination: DetailView()
.environmentObject(model)
.onAppear() {
model.fetchProfiles()
}
) {
ListRow(home:model.lists[index])
.environmentObject(model)
}
.isDetailLink(false)
}
}
}
}
DetailView Card
struct ProfilesCard: View {
#EnvironmentObject var model: Model
var body: some View {
VStack(alignment: .trailing, spacing: 16) {
if !model.sortedProfiles.isEmpty {
VStack(alignment: .leading, spacing: 16) {
ForEach(model.sortedProfiles.indices, id: \.self) { index in
ProfileRow(
name: "\(model.sortedProfiles[index].firstName) \(model.sortedProfiles[index].lastName)",
imgPath: model.sortedProfiles[index].imgPath,
index: index)
.environmentObject(model)
}
}
.padding(.top, 16)
}
}//End of Card
.modifier(Card())
.onAppear() {
print("CARD DID APPEAR: Profiles \(model.profiles)")
print("CARD DID APPEAR: SORTED \(model.sortedTenants)")
}
}
}
struct ProfileRow: View {
#EnvironmentObject var model: Model
#State var imageURL = URL(string: "")
var name: String
var imgPath: String
var index: Int
private func loadImage() {
print("load image: \(imgPath)")
DispatchQueue.main.async {
fm.getURLFromFirestore(path: imgPath, success: { (imgURL) in
print("Image URL: \(imgURL)")
imageURL = imgURL
}) { (error) in
print(error)
}
}
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(alignment: .center, spacing: 12) {
KFImage(imageURL,options: [.transition(.fade(0.2)), .forceRefresh])
.placeholder {
Rectangle().foregroundColor(.gray)
}
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 32, height: 32)
.cornerRadius(16)
// Profile text is always displayed correctly
Text(name)
.modifier(BodyText())
.frame(maxWidth: .infinity, alignment: .leading)
}
}
.onAppear() {
print("CARD ROW")
// Crashes if check is not there
if model.profiles.count > 0 {
print("CARD ROW DID APPEAR: Profiles \(model.profiles[0]. imgPath)")
print("CARD ROW DID APPEAR: Sorted \(model.sortedProfiles[0].imgPath)")
}
loadImage()
}
}
}
Model
class Model: ObservableObject {
init() {
fetchData()
}
#Published var profiles: [Profile] = []
var sortedProfiles: [Profile] {return profiles.removeDuplicates }
#Published var list: List? {
didSet {
fetchProfiles()
}
}
func fetchData() {
if let currentUser = Auth.auth().currentUser {
email = currentUser.email!
db.collection("lists")
.whereField("createdBy", isEqualTo: currentUser.uid)
.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
return
}
self.lists = documents.compactMap { queryDocumentSnapshot -> List? in
return try? queryDocumentSnapshot.data(as: List.self)
}
}
}
}
func fetchProfiles() {
profiles.removeAll()
for p in list!.profiles {
firestoreManager.fetchProfile(uid: t, completion: { [self] profile in
profiles.append(profile)
})
}
}
}
Update
What I have tried so far is to use didSet for the ImgPath or ImgURL but still not luck. Also have tried using model.profiles directly.

In all callbacks with Firestore API make assignment for published or state properties on main queue, because callback might be called on background queue.
So, assuming data is returned and parsed correctly, here is as it should look like
for p in list!.profiles {
firestoreManager.fetchProfile(uid: t, completion: { [self] profile in
DispatchQueue.main.async {
profiles.append(profile)
}
})
}
also I would recommend to avoid same naming for your custom types with SDK types - there might be very confusing non-obvious errors
// List model below might conflict with SwiftUI List
return try? queryDocumentSnapshot.data(as: List.self)

As per my knowledge its not the problem from firebase end, because the ones data fetched the new data is updated. You are facing problem of image caching. Caching is a technique that stores a copy of a given resource. So when the image is loaded for first time it get cached and whenever you are reloading images are displayed from cache instead of loading from URL. This is done for more network usage.
You can programatically clear cache by adding following code before your image loading.
Alamofire uses NSURLCache in the background so you just have to call:
NSURLCache.sharedURLCache().removeAllCachedResponses()
Update for Swift 4.1
URLCache.shared.removeAllCachedResponses()

Related

In SwiftUI, how do I trigger a function after loading collection from Firebase?

My app loads a simple collection of .ics URLs from Firebase. I then download and parse each calendar file to save in an array of structs.
This code works:
struct Calendar: Identifiable, Codable {
#DocumentID var id: String?
var urlString: String = ""
var name: String = ""
var licenseNumber: String = ""
}
struct ScheduleView: View {
#FirestoreQuery(collectionPath: "calendars") var calendars: [Calendar]
#State var matches: [Match] = []
var body: some View {
NavigationView {
VStack {
List(calendars) { calendar in
Text (calendar.urlString)
}
List(matches) { match in
Text (match.dateString)
}
Spacer()
Button {
downloadMatchCalendars()
} label: {
Text ("Download calendars")
}
}
}
}
func downloadMatchCalendars() { /* code that correctly populates matches */ }
}
List(calendars) is immediately visible when I launch and it updates when the Firebase collection changes. But, I can't get List(matches) to update, too. It's blank until I click the button. And if a new calendar is added in Firebase, the new matches are not loaded/displayed.
I tried using NavigationView{}.onAppear(perform: dowloadMatchCalendars). I tried a didSet on my #FirestoreQuery. Neither worked.
How can I run downloadMatchCalendars after loading the calendars from Firebase.
(See next post for my second related question.)

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).

How to link sing in to new page

Once I have signed into my app I display ext saying signed in but I want to link it to a new view in a separate file
How can I do this?
Sign in simplified page ...
struct ContentView: View {
#EnvironmentObject var viewModel: AppViewModel
var body: some View {
NavigationView{
if viewModel.signedIn{
VStack{
Text("Signed In")//I would like this to go to a homepage and not just say text
Button(action: {
viewModel.signOut()
}, label: {
Text("Sign Out")
.foregroundColor(.blue)
.background(Color(.green))
})
}
}
else{
SignInView()
}
}
.onAppear {
viewModel.signedIn = viewModel.isSignedIn
}
}
}
How can I active this whilst still being able to access my sign out button
Use NavigationLink programatically, from apple docs:
Optionally, you can use a navigation link to perform navigation
programmatically. You do so in one of two ways:
#State private var shouldShowPurple = false
Then you can modify the purple navigation link to bind to the state
variable:
NavigationLink(
"Purple",
destination: ColorDetail(color: .purple),
isActive: $shouldShowPurple)
In your case, isSignedIn should be used in isActive to trigger the navigation.

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