I am trying to fetch specific Firebase data with a for loop but it gives me this error: "'ListenerRegistration' cannot be used as a type conforming to protocol 'View' because 'View' has static requirements". The Code runs if the ZStack{}.onAppear() function is called. How can I change that?
ForEach(self.allFriends, id: \.id) { friend in
Firestore.firestore().collection(friend.email+"-TodayCal").addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("error fetching: \(error!)")
return
}
let name = documents.map { $0["nameOfFriend"] }
print(name)
self.allFriendsCal.removeAll()
for i in 0..<name.count {
self.allFriendsCal.append(AllFriendsCal(id: UUID(uuidString: documents[i].documentID) ?? UUID(), name: name[i] as? String ?? "Failed to get Name"))
}
}
}
ForEach is a View type, which takes in other Views in the function builder. You can achieve what you want by calling the Firebase collection in onAppear() or wherever else you already have allFriends populated and iterating through the results like so:
.onAppear {
for friend in self.allFriends {
Firestore.firestore().collection(friend.email+"-TodayCal").addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("error fetching: \(error!)")
return
}
let name = documents.map { $0["nameOfFriend"] }
print(name)
self.allFriendsCal.removeAll()
for i in 0..<name.count {
self.allFriendsCal.append(AllFriendsCal(id: UUID(uuidString: documents[i].documentID) ?? UUID(), name: name[i] as? String ?? "Failed to get Name"))
}
}
}
}
Related
I have an app Im trying to build with sleep data for sleep apnea. I successfully setup CoreData with some help and its saving the first user input from the textfields and displaying it in the log from CoreData (changing the strings to Doubles was a challenge but success). However, whenever I put another 2nd input in, it saves and displays the first input again. It won't change. What am I doing wrong? A side note, it also is not creating a unique ID for each entry for some reason. Thank you guys....you'd been awesome so far helping as Im a newbie.
SleepModel.swift
struct SleepModel: Identifiable, Hashable {
var id = UUID().uuidString // ID variable
var hoursSlept: Double // Hours slept variable
var ahiReading: Double // AHI variable
}
Save function in AddEntryView.swift which is called when the button is pushed
private func saveSleepModel() {
guard let hoursSleptDouble = Double(hoursSleptString) else {
print("hours slept is invalid")
return
}
guard let ahiReadingDouble = Double(ahiReadingString) else {
print("ahi reading is invalid")
return
}
// Creates Sleep Model for user input & sends parameters
var sleepModel = SleepModel(hoursSlept: hoursSleptDouble, ahiReading: ahiReadingDouble)
coreDataViewModel.saveRecord(sleepModel: sleepModel) {
print("Success")
}
}
CoreDataViewModel
import SwiftUI
import CoreData
class CoreDataViewModel: ObservableObject {
let container: NSPersistentContainer
#Published var savedRecords: [SleepEntity] = []
init() {
container = NSPersistentContainer(name: "SleepContainer")
container.loadPersistentStores { description, error in
if let error = error {
print("Error loading contaienr - \(error.localizedDescription)")
} else {
print("Core data loaded successfully")
}
}
fetchRecords()
}
func saveRecord(sleepModel: SleepModel, onSuccess: #escaping() -> Void) {
let newRecord = SleepEntity(context: container.viewContext)
newRecord.hoursSlept = sleepModel.hoursSlept
newRecord.ahiReading = sleepModel.ahiReading
saveData(onSuccess: onSuccess)
}
func saveData(onSuccess: #escaping() -> Void) {
do {
try container.viewContext.save()
fetchRecords()
onSuccess()
} catch let error {
print("Error saving items: \(error)")
}
}
func fetchRecords() {
let request = NSFetchRequest<SleepEntity>(entityName: "SleepEntity")
request.sortDescriptors = [
NSSortDescriptor(keyPath: \SleepEntity.id, ascending: false)
]
do {
savedRecords = try container.viewContext.fetch(request)
} catch let error {
print("Error fetching requests - \(error.localizedDescription)")
}
}
}
I'm trying to retrieve data from documents in a subCollection in Firestore, but it returns an empty array after executing it.
I tried to do the following.
#Published var OrdersID = [String]()
func fetchOrdersID(){
FirebaseManager.shared.firestore.collection("Users").document(id).collection("OrdersID").getDocuments { snapshot, error in
guard let documents = snapshot?.documents else {
print("No documents")
return
}
DispatchQueue.main.async {
self.OrdersID = documents.map{ d in
return d["id"] as? String ?? ""
}
}
}
}
but still it returns an empty array of OrdersID outside this function, and inside of it returns the proper array list(IMAGE 1 shows this, orders ID is the content of the OrdersID array IMAGE 1)
You could try escaping with a completion..
#Published var OrdersID = [String]()
func fetchOrdersID(withCompletion completion: #escaping ((_ orderID: String) -> (Void))) {
FirebaseManager.shared.firestore.collection("Users").document(id).collection("OrdersID").getDocuments { snapshot, error in
guard let documents = snapshot?.documents else {
print("No documents")
return
}
let orderID = documents["id"] as? String ?? ""
completion(orderID)
}
}
And then when you go to call, you can add to the OrdersId...
fetchOrdersID { orderID in
OrdersID.append(orderID)
}
How I can get and display last message in my chat?
For test, I created four users with test messages. Now I can display only last message for all users. I mark red color.
Also I use firebase to save messages and create channels.
Struct in firebase look like this:
- Chats
- channel id
- document data (then be stored ID and NAME of channel)
- collection thread
- documents data (then be stored MESSAGES)
My struct in channel:
struct Channel {
let id: String?
let name: String
init(name: String) {
id = nil
self.name = name
}
init?(document: DocumentSnapshot) {
let data = document.data()!
guard let name = data["name"] as? String else {
return nil
id = document.documentID
self.name = name
}
}
extension Channel: DatabaseRepresentation {
var representation: [String : Any] {
var rep = ["name": name]
if let id = id {
rep["id"] = id
}
return rep
}
}
And my struct message, I use MessageKit:
struct Message: MessageType {
let id: String?
let content: String
let sentDate: Date
let sender: SenderType
var kind: MessageKind {
if let image = image {
return .photo(ImageMediaItem.init(image: image))
} else {
return .text(content)
}
}
var messageId: String {
return id ?? UUID().uuidString
}
var image: UIImage? = nil
var downloadURL: URL? = nil
init(profile: Profile, content: String) {
sender = Sender(id: profile.id, displayName: profile.name)
self.content = content
sentDate = Date()
id = nil
}
init?(document: QueryDocumentSnapshot) {
let data = document.data()
guard let sentDate = (data["created"] as? Timestamp)?.dateValue() else {
return nil
}
guard let senderID = data["senderID"] as? String else {
return nil
}
guard let senderName = data["senderName"] as? String else {
return nil
}
id = document.documentID
self.sentDate = sentDate
sender = Sender(id: senderID, displayName: senderName)
if let content = data["content"] as? String {
self.content = content
downloadURL = nil
} else if let urlString = data["url"] as? String, let url = URL(string: urlString) {
downloadURL = url
content = ""
} else {
return nil
}
}
}
extension Message: DatabaseRepresentation {
var representation: [String : Any] {
var rep: [String : Any] = [
"created": sentDate,
"senderID": sender.senderId,
"senderName": sender.displayName
]
if let url = downloadURL {
rep["url"] = url.absoluteString
} else {
rep["content"] = content
}
return rep
}
}
For load my chennels I use code below:
fileprivate func observeQuery() {
guard let query = query else { return }
listener = query.addSnapshotListener { (snapshot, error) in
guard let snapshot = snapshot else {
print("Error listening for channel updates: \(error?.localizedDescription ?? "No error")")
return
}
snapshot.documentChanges.forEach { (change) in
self.handleDocumentChange(change)
}
}
}
private func handleDocumentChange(_ change: DocumentChange) {
guard let channel = Channel(document: change.document) else {
return
}
switch change.type {
case .added:
addChannelToTable(channel)
case .modified:
updateChannelInTable(channel)
case .removed:
removeChannelFromTable(channel)
}
}
private func addChannelToTable(_ channel: Channel) {
guard !channels.contains(channel) else {
return
}
channels.append(channel)
channels.sort()
guard let index = channels.index(of: channel) else {
return
}
tableView.insertRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
private func updateChannelInTable(_ channel: Channel) {
guard let index = channels.index(of: channel) else {
return
}
channels[index] = channel
tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
private func removeChannelFromTable(_ channel: Channel) {
guard let index = channels.index(of: channel) else {
return
}
channels.remove(at: index)
tableView.deleteRows(at: [IndexPath(row: index, section: 0)], with: .automatic)
}
I think need update my Channel struct. But how to do it?
And how to correct load and display last message from firebase?
If need more info pls tell me, I will update my question.
If the question is how to get only the last message from Firestore, you need to define how to determine what the last message is. That's usually done via a timestamp - the latest timestamp will be the last message.
The structure in the question is a little unclear so let me provide a simple example.
messages //collection
document_0 //documentID auto-generated
msg: "Last Message"
timestamp: "20191201"
document_1
msg: "First message"
timestamp: "20190801"
document_2
msg: "A message in the middle"
timestamp: "20191001"
As you can see, no matter what order they are written to Firestore, it's clear that the one with the latest timestamp (20191201 ) is the last message.
To get the last message we need a query that does two things:
1) Query the messages node, sort descending, which will put the last message 'at the top'
2) Limit the query to 1, which will get that message.
func readLastMessage() {
let ref = Firestore.firestore().collection("messages").order(by: "timestamp", descending: true).limit(to: 1)
ref.getDocuments(completion: { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
if let doc = snapshot.documents.first {
let docID = doc.documentID
let msg = doc.get("msg")
print(docID, msg)
}
})
}
and the output
Last Message
The above code gets the last message but could be expanded upon by adding an observer instead of getDocuments in the same fashion that will notify the app when there's a new last message.
When I update the firebase firestore database with any new field, it instantly kills any app running that uses the data with the fatal error in the code below.
The error I get says "fatalError: "Unable to initialize type Restaurant with dictionary [(name: "test", availability: "test", category: "test")]
I'd like to be able to update it without having the apps crash. If that happens, they have to delete and reinstall the app to get it to work again, so I think it's storing the data locally somehow, but I can't find where.
What can I do to make this reset the data or reload without crashing?
The file where the error is thrown (when loading the table data):
fileprivate func observeQuery() {
stopObserving()
guard let query = query else { return }
stopObserving()
listener = query.addSnapshotListener { [unowned self] (snapshot, error) in
guard let snapshot = snapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
let models = snapshot.documents.map { (document) -> Restaurant in
if let model = Restaurant(dictionary: document.data()) {
return model
} else {
// Don't use fatalError here in a real app.
fatalError("Unable to initialize type \(Restaurant.self) with dictionary \(document.data())")
}
}
self.restaurants = models
self.documents = snapshot.documents
if self.documents.count > 0 {
self.tableView.backgroundView = nil
} else {
self.tableView.backgroundView = self.backgroundView
}
self.tableView.reloadData()
}
}
And the Restaurant.swift file:
import Foundation
struct Restaurant {
var name: String
var category: String // Could become an enum
var availability: String // from 1-3; could also be an enum
var description: String
var dictionary: [String: Any] {
return [
"name": name,
"category": category,
"availability": availability,
"description": description
]
}
}
extension Restaurant: DocumentSerializable {
//Cities is now availability
static let cities = [
"In Stock",
"Back Order",
"Out of Stock"
]
static let categories = [
"Rock", "Boulder", "Grass", "Trees", "Shrub", "Barrier"
]
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let category = dictionary["category"] as? String,
let availability = dictionary["availability"] as? String,
let description = dictionary["description"] as? String
else { return nil }
self.init(name: name,
category: category,
availability: availability,
description: description
)
}
}
The Local Collection File with the Document.Serializable code:
import FirebaseFirestore
// A type that can be initialized from a Firestore document.
protocol DocumentSerializable {
init?(dictionary: [String: Any])
}
final class LocalCollection<T: DocumentSerializable> {
private(set) var items: [T]
private(set) var documents: [DocumentSnapshot] = []
let query: Query
private let updateHandler: ([DocumentChange]) -> ()
private var listener: ListenerRegistration? {
didSet {
oldValue?.remove()
}
}
var count: Int {
return self.items.count
}
subscript(index: Int) -> T {
return self.items[index]
}
init(query: Query, updateHandler: #escaping ([DocumentChange]) -> ()) {
self.items = []
self.query = query
self.updateHandler = updateHandler
}
func index(of document: DocumentSnapshot) -> Int? {
for i in 0 ..< documents.count {
if documents[i].documentID == document.documentID {
return i
}
}
return nil
}
func listen() {
guard listener == nil else { return }
listener = query.addSnapshotListener { [unowned self] querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
let models = snapshot.documents.map { (document) -> T in
if let model = T(dictionary: document.data()) {
return model
} else {
// handle error
fatalError("Unable to initialize type \(T.self) with local dictionary \(document.data())")
}
}
self.items = models
self.documents = snapshot.documents
self.updateHandler(snapshot.documentChanges)
}
}
func stopListening() {
listener = nil
}
deinit {
stopListening()
}
}
fatalError: "Unable to initialize type Restaurant with dictionary [(name: "test", availability: "test", category: "test")]
Seems pretty straightforward - that dictionary does not contain enough information to create a Restaurant object.
The error is from
if let model = Restaurant(dictionary: document.data()) {
return model
} else {
// Don't use fatalError here in a real app.
fatalError("Unable to initialize type \(Restaurant.self) with dictionary \(document.data())")
}
because your initializer returns a nil value, from:
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let category = dictionary["category"] as? String,
let availability = dictionary["availability"] as? String,
let description = dictionary["description"] as? String
else { return nil }
self.init(name: name,
category: category,
availability: availability,
description: description
)
}
because your guard is returning nil because you do not have a description key in the dictionary.
To fix, either put a description key in the dictionary OR change your initializer to use a default description when the key is missing.
For example, here is your initializer, rewritten to use a default description, for when the description entry is missing
init?(dictionary: [String : Any]) {
guard let name = dictionary["name"] as? String,
let category = dictionary["category"] as? String,
let availability = dictionary["availability"] as? String
else { return nil }
let description = dictionary["description"] as? String
let defaultDescription: String = description ?? "No Description"
self.init(name: name,
category: category,
availability: availability,
description: defaultDescription
)
}
I'm using FirebaseFirestore for my database.
I have an array of Lists with a name and description. I'd also like to grab each lists unique documentId. Is this possible?
List.swift
struct List {
var name:String
var description:String
var dictionary:[String:Any] {
return [
"name":name,
"description":description
]
}
}
ListTableViewController.swift
func getLists() {
if Auth.auth().currentUser != nil {
db.collection("lists").getDocuments { (querySnapshot, error) in
if let error = error {
print("Error getting documents: \(error)")
} else {
self.listArray = querySnapshot!.documents.flatMap({List(dictionary: $0.data())})
DispatchQueue.main.async {
self.tableView.reloadData()
}
}
}
}
}
I found you can get the documentId by doing...
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
}
But how do I store this in my List to be able to call later?
1.Suppose you are getting documents in snapshot.
guard let documents = ducomentSnapshot?.documents else {
print("Error fetching documents: \(error!)")
return
}
// Get documentId of each document objects using this code.
for i in 0 ..< documents.count {
let dictData = documents[i].data()
let documentID = documents[i].documentID
print("Document ID \(documentID)")
}
I'm not positive if this is the best way but I appended to the listArray and it seems to work.
self.listArray.append(List(name: document["name"] as! String, description: document["description"] as! String, documentId: document.documentID))
Add an id attribute to your list struct:
struct List {
var name:String
var description:String
var id: String?
var dictionary:[String:Any] {
return [
"name":name,
"description":description
]
}
}
Then when mapping your documents manually assign the id attribute:
list = documents.compactMap{ (queryDocumentSnapshot) -> List? in
var listItem = try? queryDocumentSnapshot.data(as: List.self)
listItem?.id = queryDocumentSnapshot.documentID
return listItem
}