I'm new to Swift and I'm having trouble sending data in my Firestore database to my global data structure (array in a struct). I looked through many similar questions and tried their solutions for hours to no avail, so please help me. I am totally stuck.
I somewhat understand that the problem lies in the initializeDocs() section of my code being asynchronous. I've tried DispatchQueue.main.async as well as completion handlers (although I'm not sure if the way I did them was correct), but I'm still getting the same result- i.e. the Firestore data is not being saved in the struct and I am instead getting back an empty array when I try calling them in my init function. Any help is appreciated.
Here is my code:
struct booksStruc{
static var modelAry = [Model]()
static var titles: [String] = []
static var authors: [String] = []
static var years: [String] = []
static var emails: [String] = []
static var capacity = 0
static var counter = 0
}
init(logo:String, title:String, author:String, year:String, email:String, desc:String) {
let group = DispatchGroup()
ref = Database.database().reference()
super.init()
group.enter()
DispatchQueue.main.async{
self.initializeDocs()
group.leave()
}
self.logoTitle = logo
self.imageTitle = title
self.imageAuthor = author
self.imageYear = year
self.imageEmail = email
self.imageDesc = desc
print("end of init's titles are ", booksStruc.titles)
print("end of init's capacity is ", booksStruc.capacity)
}
func initializeDocs() {
db.collection("books").getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
let downloadGroup = DispatchGroup()
downloadGroup.enter()
DispatchQueue.main.async{
for document in querySnapshot!.documents {
let dictionary = document.data()
booksStruc.capacity = document.data().capacity
//print("capacity is: ", capacity)
for(key, value) in dictionary{
if(key == "title"){
booksStruc.titles.append(value as! String)
} else if(key == "author"){
booksStruc.authors.append(value as! String)
} else if(key == "year"){
booksStruc.years.append(value as! String)
} else if(key == "email"){
booksStruc.emails.append(value as! String)
}
//print("\(key) : \(value)")
}
}
downloadGroup.leave()
}
}
}
print("titles are ", booksStruc.titles)
print("capacity is ", booksStruc.capacity)
}
Related
I have this way of collecting information.
struct MainText {
var mtext: String
var memoji: String
}
class MainTextModel: ObservableObject {
#Published var maintext : MainText!
init() {
updateData()
}
func updateData() {
let db = Firestore.firestore()
db.collection("maintext").document("Main").getDocument { (snap, err) in
if err != nil{
print((err?.localizedDescription)!)
return
}
let memoji = snap?.get("memoji") as! String
let mtext = snap?.get("mtext") as! String
DispatchQueue.main.async {
self.maintext = MainText(mtext: mtext, memoji: memoji)
}
}
}
}
And such a way of displaying.
#ObservedObject private var viewModel = MainTextModel()
self.viewModel.maintext.memoji
self.viewModel.maintext.mtext
How can I update online without rebooting the view?
Instead of using getDocument, which only gets the document once and doesn't return updates, you'll want to add a snapshot listener.
Here's the Firestore documentation for that: https://firebase.google.com/docs/firestore/query-data/listen
In your case, you'll want to do something like:
db.collection("maintext").document("Main")
.addSnapshotListener { documentSnapshot, error in
guard let document = documentSnapshot else {
print("Error fetching document: \(error!)")
return
}
guard let data = document.data() else {
print("Document data was empty.")
return
}
if let memoji = data["memoji"] as? String, let mtext = data["mtext"] as? String {
self.maintext = MainText(mtext: mtext, memoji: memoji)
}
}
I wrote the function to lad the records from firebase but there's an error
Escaping closure captures mutating 'self' parameter
The function is written as follows:
let db = Firestore.firestore()
#State var libraryImages: [LibraryImage] = []
mutating func loadImages() {
libraryImages = []
db.collection(K.FStore.CollectionImages.collectionName).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
if let snapshotDocuments = querySnapshot?.documents {
for document in snapshotDocuments {
let documentData = document.data()
let title: String = documentData[K.FStore.CollectionImages.title] as! String
let thumbnailUrl: String = documentData[K.FStore.CollectionImages.thumbnailUrl] as! String
let svgUrl: String = documentData[K.FStore.CollectionImages.svgUrl] as! String
let libraryImageItem = LibraryImage(title: title, thumbnailUrl: thumbnailUrl, svgUrl: svgUrl)
self.libraryImages.append(libraryImageItem)
}
}
}
}
}
Does anyone know what is causing an error and how to get rid of it?
Move all this into reference type view model and use it as observed object in your view
Here is a demo of possible approach:
struct DemoView: View {
#ObservedObject var vm = ImagesViewModel()
// #StateObject var vm = ImagesViewModel() // << SwiftUI 2.0
var body: some View {
Text("Loaded images: \(vm.libraryImages.count)")
.onAppear {
self.vm.loadImages()
}
}
}
class ImagesViewModel: ObservableObject {
let db = Firestore.firestore()
#Published var libraryImages: [LibraryImage] = []
func loadImages() {
libraryImages = []
db.collection(K.FStore.CollectionImages.collectionName).getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
} else {
if let snapshotDocuments = querySnapshot?.documents {
var images = [LibraryImage]()
for document in snapshotDocuments {
let documentData = document.data()
let title: String = documentData[K.FStore.CollectionImages.title] as! String
let thumbnailUrl: String = documentData[K.FStore.CollectionImages.thumbnailUrl] as! String
let svgUrl: String = documentData[K.FStore.CollectionImages.svgUrl] as! String
let libraryImageItem = LibraryImage(title: title, thumbnailUrl: thumbnailUrl, svgUrl: svgUrl)
images.append(libraryImageItem)
}
DispatchQueue.main.async {
self.libraryImages = images
}
}
}
}
}
}
This code adds all the data in a single array. In HomeViev I use to Foreach and I added to data to list. But I have to split the data in two. status collection has two type "active" and "closed" but I don't know how can I filter
import SwiftUI
import Combine
import Firebase
let dbCollection = Firestore.firestore().collection("Signals")
class FirebaseSession : ObservableObject {
#Published var session: User? { didSet { self.didChange.send(self) }}
#Published var data = [Signal]()
var didChange = PassthroughSubject<FirebaseSession, Never>()
var handle: AuthStateDidChangeListenerHandle?
func listen () {
handle = Auth.auth().addStateDidChangeListener { (auth, user) in
if let user = user {
print("Got user: \(user)")
self.session = User(uid: user.uid, email: user.email)
self.readData()
} else {
self.session = nil
}
}
}
func readData() {
dbCollection.addSnapshotListener { (documentSnapshot, err) in
if err != nil {
print((err?.localizedDescription)!)
return
}else {
print("read data success")
}
documentSnapshot!.documentChanges.forEach { i in
// Read real time created data from server
if i.type == .added {
let id = i.document.documentID
let symbol = i.document.get("symbol") as? String ?? ""
let status = i.document.get("status") as? String ?? ""
self.data.append(Signal(id: id, symbol: symbol, status: status))
}
// Read real time modify data from server
if i.type == .modified {
self.data = self.data.map { (eachData) -> Signal in
var data = eachData
if data.id == i.document.documentID {
data.symbol = i.document.get("symbol") as! String
data.status = i.document.get("status") as? String ?? ""
return data
}else {
return eachData
}
}
}
// When data is removed...
if i.type == .removed {
let id = i.document.documentID
for i in 0..<self.data.count{
if self.data[i].id == id{
self.data.remove(at: i)
return
}
}
}
}
}
}
}
The question states
But I have to split the data in two
I assume that means two arrays; one for active and one for closed.
var activeData = [...
var closedData = [...
There are a couple of ways to do that
1)
Query Firestore for all status fields equal to active and load those documents into the active array and then another query for status fields equal closed and load those in the the closed array
2)
I would suggest a simpler approach
if i.type == .added {
let id = i.document.documentID
let symbol = i.document.get("symbol") as? String ?? ""
let status = i.document.get("status") as? String ?? ""
if status == "active" {
self.activeData.append(Signal(id: id, symbol: symbol, status: status))
} else {
self.closedData.append(Signal(id: id, symbol: symbol, status: status))
}
}
and do the same thing within .modified and .removed; identify the status so the code will know which array to remove it from.
EDIT:
Based on a comment
I don't know how to query this codes.
I am providing code to query for signals that are active. This code will return only active signals and as signals become active, inactive etc, this will modify a signalArray to stay in sync with the data.
let dbCollection = Firestore.firestore().collection("Signals")
let query = dbCollection.whereField("status", isEqualTo: "active").addSnapshotListener( { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let signalToAdd = Signal(withDoc: diff.document)
self.signalArray.append(signalToAdd)
}
if (diff.type == .modified) {
let docId = diff.document.documentID
if let indexOfSignalToModify = self.signalArray.firstIndex(where: { $0.signal_id == docId} ) {
let signalToModify = self.signalArray[indexOfSignalToModify]
signalToModify.updateProperties(withDoc: diff.document)
}
}
if (diff.type == .removed) {
let docId = diff.document.documentID
if let indexOfSignalToRemove = self.signalArray.firstIndex(where: { $0.signal_id == docId} ) {
self.signalArray.remove(at: indexOfSignalToRemove)
}
}
}
})
Note that my Signal Class has an initializer that accepts a QueryDocumentSnapshot to initialize it as well as a .updateProperties function to update its internal properties.
So I am trying to retrieve users follower information within an array. Then with that array get each users posts and then append them in my table view. All throughout this, I would like a snapshot listener to be added so that if a user likes a post the number will auto update. When I do this tho it appends every single update so one post will be shown about 5 times after an action such as liking it is performed which I do not want to happen. Could someone help me figure this out? I am using Xcode Swift. Thanks in advance!
class Posts {
var postArray = [UserPost]()
var db: Firestore!
init() {
db = Firestore.firestore()
}
func loadData(completed: #escaping () -> ()) {
let sevenDaysAgo = Calendar.current.date(byAdding: .day, value: -7, to: Date())
self.postArray = []
guard let user = Auth.auth().currentUser else { return }
let displayUsername = user.displayName
let userReference = db.collection("Users").document("User: \(displayUsername!)").collection("Connect").document("Following")
userReference.getDocument { (document, error) in
if let documentData = document?.data(),
var FollowerArray = documentData["Following"] as? [String] {
FollowerArray.append(displayUsername!)
FollowerArray.forEach {
self.db.collection("Users").document("User: \($0)").collection("Posts").whereField("timeOfPost", isGreaterThanOrEqualTo: sevenDaysAgo!)
.addSnapshotListener { (querySnapshot, error) in
guard error == nil else {
print("*** ERROR: adding the snapshot listener \(error!.localizedDescription)")
return completed()
}
//self.postArray = []
// there are querySnapshot!.documents.count documents in the posts snapshot
for document in querySnapshot!.documents {
let post = UserPost(dictionary: document.data())
self.postArray.append(post)
}
completed()
}
}
}
I would suggest a different approach and enable Firestore to tell you when child nodes (posts) have been added, modified or removed. Based on your code your structure is something like this:
Users
uid
//some user data like name etc
Posts
post_0
likes: 0
post: "some post 0 text"
post_1
likes: 0
post: "text for post 1"
Let's have a class to store the Post in
class UserPostClass {
var postId = ""
var postText = ""
var likes = 0
init(theId: String, theText: String, theLikes: Int) {
self.postId = theId
self.postText = theText
self.likes = theLikes
}
}
and then an array to hold the UserPosts which will be the tableView dataSource
var postArray = [UserPostClass]()
then.. we need a block of code to do three things. First, when a new post is added to the database (or when we first start the app), add it to the dataSource array. Second, when a post is modified, for example another user likes the post, update the array to reflect the new like count. Third, if a post is deleted, remove it from the array. Here's the code that does all three......
func populateArrayAndObservePosts() {
let uid = "uid_0" //this is the logged in user
let userRef = self.db.collection("users").document(uid)
let postsRef = userRef.collection("Posts")
postsRef.addSnapshotListener { documentSnapshot, error in
guard let snapshot = documentSnapshot else {
print("err fetching snapshots")
return
}
snapshot.documentChanges.forEach { diff in
let doc = diff.document
let postId = doc.documentID
let postText = doc.get("post") as! String
let numLikes = doc.get("likes") as! Int
if (diff.type == .added) { //will initially populate the array or add new posts
let aPost = UserPostClass(theId: postId, theText: postText, theLikes: numLikes)
self.postArray.append(aPost)
}
if (diff.type == .modified) { //called when there are changes
//find the post that was modified by it's postId
let resultsArray = self.postArray.filter { $0.postId == postId }
if let postToUpdate = resultsArray.first {
postToUpdate.likes = numLikes
}
}
if (diff.type == .removed) {
print("handle removed \(postId)")
}
}
//this is just for testing. It prints all of the posts
// when any of them are modified
for doc in snapshot.documents {
let postId = doc.documentID
let postText = doc.get("post") as! String
let numLikes = doc.get("likes") as! Int
print(postId, postText, numLikes)
}
}
}
I have a data struct which contains some string parameters. The struct is below:
struct pulledMessage{
var convoWithUserID: String
var convoWithUserName: String
}
I have a function which assigns a value to variables based on the values within a particular pulledMessage. For some more complicated, out-of-the-scope-of-the-question, reasons, these values come from [pulledMessage] array. The pulledMessage always changes in the actual function but for illustration purposes I will write it as a constant:
var messageArray = [pulledMessage]()
func assignValues(){
messageArray.append(pulledMessage(convoWithUserID: "abc123", convoWithUserName: "Kevin"))
let convoWithUserID = messageArray[0].convoWithUserID
let convoWithUserName = messageArray[0].convoWithUserName
print(convoWithUserID) //returns optional("abc123")
print(convoWithUserName) // returns optional("Kevin")
}
I have tried adding ! to unwrap the values in different ways:
messageArray[0]!.convoWithUserID
This tells gives me an error that I cannot unwrap a non-optional type of pulledMessage.
messageArray[0].convoWithUserID!
This gives me an error that I cannot unwrap a non-optional type of String.
This stack question suggests utilizing if let to get rid of the optional:
if let convoWithUserIDCheck = messageArray[0].convoWithUserID{
convoWithUserID = convoWithUserIDCheck
}
This gives me a warning that there is no reason to do if let with a non-optional type of string. I have no idea how to get it to stop returning the values wrapped by optional().
Update: The more complicated, complete code
The SQL Database functions:
class FMDBManager: NSObject {
static let shared: FMDBManager = FMDBManager()
let databaseFileName = "messagesBetweenUsers.sqlite"
var pathToDatabase: String!
var database: FMDatabase!
override init() {
super.init()
let documentsDirectory = (NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] as NSString) as String
pathToDatabase = documentsDirectory.appending("/\(databaseFileName)")
}
func loadMessageData(){//will need a struct to load the data into a struct
if openDatabase(){
let query = "select * from messages order by messageNumber asc"
do{
print(database)
let results: FMResultSet = try database.executeQuery(query, values: nil)
while results.next(){
let message = pulledMessage(convoWithUserID: String(describing: results.string(forColumn: "convoWithUserID")), convoWithUserName: String(describing: results.string(forColumn: "convoWithUserName")), messageString: String(describing: results.string(forColumn: "messageString")), senderID: String(describing: results.string(forColumn: "senderID")), timeSent: String(describing: results.string(forColumn: "timeSent")), messageNumber: Int(results.int(forColumn: "messageNumber")))
if messagesPulled == nil{
messagesPulled = [pulledMessage]()
}
messagesPulled.append(message)
print("The message that we have pulled are \(message)")
}
}
catch{
print(error.localizedDescription)
}
database.close()
}
}
}
Running the population of the data at the onset of app launch:
func applicationDidBecomeActive(_ application: UIApplication) {
// if FMDBManager.shared.createDatabase() {
// FMDBManager.shared.insertMessageData()
// }else{
// print("Not a chance, sonny")
// FMDBManager.shared.insertMessageData()
// }
FMDBManager.shared.loadMessageData()
}
Organizing the SQL data in order:
struct pulledMessage{//global struct
var convoWithUserID: String
var convoWithUserName: String
var messageString: String
var senderID: String
var timeSent: String
var messageNumber: Int
}
var messagesPulled: [pulledMessage]!
var messageConvoDictionary = [String: [pulledMessage]]()
//For the individual message convos
var fullUnorderedMessageArray = [[pulledMessage]]()
var fullOrderedMessageArray = [[pulledMessage]]()
//For the message table
var unorderedLastMessageArray = [pulledMessage]()
var orderedLastMessageArray = [pulledMessage]()
//For the table messages... FROM HERE..........................................
func organizeSQLData(messageSet: [pulledMessage]){
var i = 0
var messageUserID = String()
while i < messageSet.count{
if (messageSet[i]).convoWithUserID != messageUserID{
print("It wasn't equal")
print(messageSet[i])
messageUserID = messageSet[i].convoWithUserID
if messageConvoDictionary[messageUserID] != nil{
messageConvoDictionary[messageUserID]?.append(messageSet[i])
}else{
messageConvoDictionary[messageUserID] = []
messageConvoDictionary[messageUserID]?.append(messageSet[i])
}
i = i + 1
}else{
messageConvoDictionary[messageUserID]?.append(messageSet[i])
i = i + 1
}
}
}
func getLastMessages(messageSet: [String:[pulledMessage]]){
for (_, messages) in messageSet{
let orderedMessages = messages.sorted(by:{ $0.timeSent.compare($1.timeSent) == .orderedAscending})
let finalMessage = orderedMessages[0]
unorderedLastMessageArray.append(finalMessage)
}
print(unorderedLastMessageArray)
}
func orderLastMessage(messageSet: [pulledMessage]){
orderedLastMessageArray = messageSet.sorted(by:{ $0.timeSent.compare($1.timeSent) == .orderedDescending})
messagesListTableView.reloadData()
print("It wasn't\(orderedLastMessageArray)")
}
func getMessagesReady(){//for observer type function calls
organizeSQLData(messageSet: messagesPulled)
getLastMessages(messageSet: messageConvoDictionary)
orderLastMessage(messageSet: unorderedLastMessageArray)
//This one is for the individual full convos for if user clicks on a cell... its done last because its not required for the page to show up
orderedFullMessageConvos(messageSet: messageConvoDictionary)
let openedMessageConversation = fullOrderedMessageArray[(indexPath.row)]//not placed in its appropriate location, but it is just used to pass the correct array (actually goes in a prepareforSegue)
}
override func viewDidLoad() {
super.viewDidLoad()
getMessagesReady()
}
Then segue to the new controller (passing openedMessageConversation to messageConvo) and run this process on a button click:
let newMessage = pulledMessage(convoWithUserID: messageConvo[0].convoWithUserID, convoWithUserName: messageConvo[0].convoWithUserName, messageString: commentInputTextfield.text!, senderID: (PFUser.current()?.objectId)!, timeSent: String(describing: Date()), messageNumber: 0)
messageConvo.append(newMessage)
let newMessageSent = PFObject(className: "UserMessages")
newMessageSent["convoWithUserID"] = newMessage.convoWithUserID
newMessageSent["convoWithUserName"] = newMessage.convoWithUserName
newMessageSent["messageString"] = newMessage.messageString
newMessageSent["senderID"] = newMessage.senderID
let acl = PFACL()
acl.getPublicWriteAccess = true
acl.getPublicReadAccess = true
acl.setWriteAccess(true, for: PFUser.current()!)
acl.setReadAccess(true, for: PFUser.current()!)
newMessageSent.acl = acl
newMessageSent.saveInBackground()
It is the newMessageSent["convoWithUserID"] and newMessageSent["convoWithUserName"] that read with the optional() in the database.
So it turns out that the reason for this stems from the function run from loadMessageData. The use of String(describing: results.string(forColumn:) requires an unwrapping of results.String(forColumn:)!. This issue propagated throughout the data modification for the whole app and caused the optional() wrapping for the print statements that I was seeing.