In SwiftUI, how do I trigger a function after loading collection from Firebase? - 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.)

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")
}
}

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.

Update the data stored on Firebase by inputting new values to TextFields where there have been default values

I designed a method to show the data stored on Firebase in the textfields according to this link. Here is my solution:
import Foundation
import SwiftUI
struct TextFieldWithDefaultValue: View {
var model: userViewModel // Actual a more complex view model
var textFieldName: String
#State var editedValue: String
init(model: userViewModel, text: String) {
self.model = model
self.textFieldName = text
switch self.textFieldName {
case "Name": self._editedValue = State(wrappedValue: model.user.name)
case "Bio": self._editedValue = State(wrappedValue: model.user.bio)
case "Interest": self._editedValue = State(wrappedValue: model.user.interest)
default: self._editedValue = State(wrappedValue: "No records")
}
}
var body: some View {
TextField(textFieldName, text: $editedValue)
}
}
One of its shortages is that I cannot bind the value of the input of object State. Therefore, the value on Firebase is never changed. So my question is, is there a shortcut that allows me to update the value whenever a user's input is different from the original one? Here is the rest of my code:
#StateObject var currentUser: userViewModel
#State private var showingAlert = false
var body: some View {
TextFieldWithDefaultValue(model: currentUser, text: "Interest")
.padding()
.keyboardType(.numberPad)
.background(Color.white.opacity(0.06))
.cornerRadius(15)
Button(action: {
currentUser.updatePersonalInfo()
self.showingAlert = true
}, label: {
Text("Save")
.foregroundColor(Color("Color"))
.fontWeight(/*#START_MENU_TOKEN#*/.bold/*#END_MENU_TOKEN#*/)
.padding(.vertical)
.frame(width: UIScreen.main.bounds.width - 100)
.clipShape(Capsule())
})
.disabled(currentUser.getName() != "" || currentUser.getInterest() != "" || currentUser.getBio() != "" ? false : true)
.opacity(currentUser.getName() != "" || currentUser.getInterest() != "" || currentUser.getBio() != "" ? 1 : 0.5)
.alert(isPresented: $showingAlert) {
() -> Alert in
Alert(title: Text("Congratulations!"), message: Text("Saved successfully!"), dismissButton: .default(Text("OK")))
}
}
A workaround is to set the StateObject variable, which I pass to the TextField with the actual value in init() function in the View.
#StateObject var currentUser: userViewModel
init(currentUser: userViewModel) {
self._currentUser = StateObject(wrappedValue: currentUser)
}
TextField("Name", text: $currentUser.user.name)
You can use onChange modifier to track changes of the text and propagate it to view model like this
TextField(textFieldName, text: $editedValue)
.onChange(of: editedValue) { newValue in
viewModel.saveToFirebase(newValue)
}
The other thing you could do is make your view model anObservableObject and add editedValue there as a #Published property. Then from the View you do this:
struct TextFieldWithDefaultValue: View {
#ObservedObject var viewModel: UserViewModel
var body: some View {
TextField(textFieldName, text: viewModel.$editedValue)
}
}
and inside the view model you can use Combine to subscribe to value’s changes and save data like this:
class UserViewModel: ObservableObject {
#Published var editedValue = “”
init(…) {
// Init `editedValue` here
$editedValue
.sink { // Save data here }
.store(in: …)
}
}
The only other thing you might want to think about is that you probably don’t want to update value on Firebase every time a single character changes. But this is already out of scope of this question

How to refresh view with fetched data - Firestore & SwiftUI

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

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