Learn Courses My Dashboard

Issue with getting value out of '.getDocuments' completion

Hi guys,

So today I was following the Swift-UI Chat App course and I ran into a problem.
In the class DatabaseService (class to manage a firebase related tasks) we used the .data(as:) extension on firebase DocumentSnapshot types in various places. This extension lets you specify an other type in which it will convert the DocumentSnapshot. But as of iOS 16, this extension wasn’t recognized anymore, so I tried to create this extension myself.

Here’s my code file: (The extension is called .dataAsChat())

import Foundation
import SwiftUI
import Contacts
import Firebase
import FirebaseStorage
import FirebaseFirestore

class DatabaseService {
    
    // The reference to the contacts view model
    @EnvironmentObject private var contactsModel: ContactsViewModel
    
    var chatListViewListeners = [ListenerRegistration]()
    var conversationViewListener = [ListenerRegistration]()
    
    // MARK: - Auth Methods
    
    /// Has a completion which gives an array with users who aren't already fetched that have the phone numbers which have been specified as an initializer
    func getPlatformUsers(localContacts: [CNContact], completion: @escaping ([User]) -> Void) {
        
        DispatchQueue.init(label: "Search-platform-users-thread").async {
            
            // The array of users we will return
            var platformUsers = [User]()
            
            // Construct an array of string phone numbers to look up
            var lookupPhoneNumbers: [[String]] = localContacts.map { contact in
                
                var allPhoneNumbers = [String]()
                
                // Turn the contact into phone number(s)
                for pn in contact.phoneNumbers {
                    
                    // Check if this phone number is already fetched
                    allPhoneNumbers.append(TextHelper.sanitizePhoneNumber(phone: pn.value.stringValue))
                }
                
                return allPhoneNumbers
            }
            /// This function lets you transform each part of an array, in this case we are going to transform every contact in the CNContact array into a string which is going to present the phone number
            
            // Make sure there are lookupPhoneNumbers
            guard lookupPhoneNumbers.count > 0 else {
                // Callback
                completion(platformUsers)
                return
            }
            
            // Retrieve the users that are on the platform
            let db = Firestore.firestore()
            
            db.collection("users").getDocuments { querySnapshot, error in
                
                // The users we've found
                var retrievedUsers = [QueryDocumentSnapshot]()
                
                /// Check for errors and that there is a snapshot
                if error == nil && querySnapshot != nil {
                    
                    /// Loop through the documents
                    querySnapshot!.documents.forEach { document in
                        
                        /// Check if the document has one of the phone numbers
                        /// Loop through the lookup phone numbers
                        lookupPhoneNumbers.forEach { phoneArray in
                            
                            /// Loop through the phone numbers in a phone array
                            for phoneNumber in phoneArray {
                                
                                /// Check if the document has the same phone number as the phone number we're looking for
                                if document["phone"] as? String == phoneNumber {
                                    
                                    /// Add the document to the query
                                    retrievedUsers.append(document)
                                    
                                    /// Remove the phoneArray from the lookupPhoneNumber to avoid errors
                                    lookupPhoneNumbers.remove(at: lookupPhoneNumbers.firstIndex(of: phoneArray)!)
                                }
                            }
                        }
                        
                    }
                    
                    // Add the documents to the platformUsers array as User objects
                    for document in retrievedUsers {
                        platformUsers.append(document.dataAsUser())
                    }
                    
                    // Return these users
                    DispatchQueue.main.async {
                        completion(platformUsers)
                    }
                    
                }
                
            }
        }
    }
    
    /// Sets the user profile if the user is logged in, with the given data and the phone number, stores the uiImage in firebase storage and provides the path to that uiImage in the firestore
    func setUserProfile(firstName: String, lastName: String, image: UIImage?, completion: @escaping (Bool) -> Void) {
        
        // Ensure that the user is logged in
        guard AuthViewModel.isUserLoggedIn() else {
            
            // Notify
            print("User is not logged in, creating profile insuccessfull")
            
            // User is not logged in
            return
        }
        
        // Get a reference to firestore
        let db =  Firestore.firestore()
        
        // Set the profile data
        let userPhone = TextHelper.sanitizePhoneNumber(phone: AuthViewModel.getLoggedInUserPhone())
        let doc = db.collection("users").document(AuthViewModel.getLoggedInUserId())
        /// Create a document with the user's id as its document id
        
        doc.setData(["firstName": firstName,
                     "lastName": lastName,
                     "phone": userPhone])
        
        // Check if an image is passed through
        if let image = image {
            
            // Create storage reference
            let storageRef = Storage.storage().reference()
            
            // Turn our image into data
            let imageData = image.jpegData(compressionQuality: 0.8)
            
            // Check if we were able to convert it into data
            guard imageData != nil else {
                return
            }
            
            // Specify the file path and file reference
            let path = "images/\(UUID().uuidString).jpg"
            let fileRef = storageRef.child(path)
            
            fileRef.putData(imageData!, metadata: nil) { metaData, error in
                
                if error == nil && metaData != nil {
                    
                    // Get full url to image
                    fileRef.downloadURL { url, error in
                        
                        // Check for errors
                        if url != nil && error == nil {
                            
                            // Set that image path to the profile
                            doc.setData(["photo": url!.absoluteString], merge: true) { error in
                                if error == nil {
                                    // Success, notify caller
                                    completion(true)
                                }
                            }
                        } else {
                            // No success in downloading url to photo
                            completion(false)
                        }
                    }
                    
                } else {
                    // Upload wasn't succesful, notify caller
                    completion(false)
                }
                
            }.resume()
        } else {
            // No image was set, doesn't matter
            completion(true)
        }
        
    }
    
    /// Checks if the user has a profile using the documentID which is the same as the uid
    func checkUserProfile(completion: @escaping (Bool) -> Void) {
        
        // Check if the user is logged in
        guard AuthViewModel.isUserLoggedIn() else {
            return
        }
        
        // Create database ref
        let db = Firestore.firestore()
        
        db.collection("users").document(AuthViewModel.getLoggedInUserId()).getDocument { snapshot, error in
            
            // TODO: Keep the user's profile data
            // TODO: Look into using Result Type to inducate failure vs profile exists
            if snapshot != nil && error == nil {
                
                // notify that profile exists
                completion(snapshot!.exists)
            } else {
                completion(false)
            }
        }
    }
    
    // MARK: - Chat Methods
    
    /// Retrieves the chats data in the database and returns it through a completion
    func getAllRelevantChats(completion: @escaping ([Chat]) -> Void) {
        
        // Get a reference to the database
        let db = Firestore.firestore()
        
        // Perform a query against the chat collection for any chats where the user is a participant
        let chatsQuery = db.collection("chats").whereField("participantids", arrayContains: AuthViewModel.getLoggedInUserId())
        
        // Perform the query
        let listener = chatsQuery.addSnapshotListener(includeMetadataChanges: true) { snapshot, error in
            
            if snapshot != nil && error == nil {
                
                var chats = [Chat]()
                
                // Loop through all the returned chat documents
                for doc in snapshot!.documents {
                    // Append the chat into the chat array
                    chats.append(doc.dataAsChat())
                }
                
                // Return the data
                completion(chats)
            } else {
                // Error in database retrieval
                print("ERROR IN DATABASE RETRIEVAL: \(error!.localizedDescription)")
            }
        }
        
        // Keep track of the listener so that we can close it later
        chatListViewListeners.append(listener)
        
    }
    
    /// This method returns all the messages of the given chat
    func getAllMessages(chat: Chat, completion: @escaping ([ChatMessage]) -> Void) {
        
        // Check that the id is not nil
        guard chat.id != nil else {
            // Can't fetch data
            completion([ChatMessage]())
            return
        }
        
        // Get a refernce to the database
        let db = Firestore.firestore()
        
        // Create the query
        let msgsQuery = db.collection("chats").document(chat.id!).collection("messages").order(by: "timestamp")
        
        // Perform the query
        let listener = msgsQuery.addSnapshotListener {  snapshot, error in
            
            if snapshot != nil && error == nil {
                
                var messages = [ChatMessage]()
                
                // Loop through the msg documents
                for doc in snapshot!.documents {
                    
                    // Parse the data into ChatMessage structs
                    let msg = doc.dataAsChatMessage()
                    
                    // Add the msg to the msgs array
                    messages.append(msg)
                    
                }
                
                // Return the results
                completion(messages)
            } else {
                // Error in database retrieval
                print("ERROR IN DATABASE RETRIEVAL: \(error!.localizedDescription)")
            }
        }
        
        // Keep track of the listener so that we can close it later
        conversationViewListener.append(listener)
        
    }
    
    /// This method will store the given message in the database
    func storeMessageInDatabase(_ message: String, chat: Chat) {
        
        // Validate chat
        guard chat.id != nil else {
            print("Error: Couldn't store message in the database because the given chat doesn't exist")
            return
        }
        
        // Get a refernce to the database
        let db = Firestore.firestore()
        
        // Get a reference to the chat
        let dbChat = db.collection("chats").document(chat.id!)
        
        // Add the message document
        dbChat.collection("messages").addDocument(data: [
            "imageurl": "",
            "msg": message,
            "senderid": AuthViewModel.getLoggedInUserId(),
            "timestamp": Date()
        ])
        
        // Update the last message of the chat
        dbChat.setData(["lastmsg": message, "updated": Date()], merge: true)
    }
    
    /// Takes a chat struct and saves the chat in the database and returns the id of the created chat in the database through a completion
    func createChat(_ chat: Chat, completion: @escaping (String) -> Void) {
        
        // Get e reference to the database
        let db = Firestore.firestore()
        
        // Create a chat document
        let newDocument = db.collection("chats").document()
        
        // Set its data
        newDocument.setData(["numparticipants": chat.numparticipants,
                             "participantids": chat.participantids,
                             "lastmsg": chat.lastmsg ?? "",
                             "updated": chat.updated ?? Date(),
                             "messages": chat.messages ?? [ChatMessage]()])
        
        // Return its document id
        completion(newDocument.documentID)
    }
    
    /// Gets the chat with the given id from the database and returns it through a completion
    func getChat(id: String, completion: @escaping (Chat) -> Void) {
        
        // Get a reference to the database
        let db = Firestore.firestore()
        
        // Get a reference to the document
        let document = db.collection("chats").document(id)
        
        // Retrieve the chat document
        document.getDocument { doc, error in
            
            if error == nil && doc != nil {
                // Return it
                completion(doc!.dataAsChat())
            }
        }
        
    }
    
    /// Removes/closes all the listeners in the chatListViewListeners array
    func closeChatListViewListeners() {
        for listener in chatListViewListeners {
            listener.remove()
        }
    }
    
    /// Removes/closes all the listeners in the conversationViewListeners array
    func closeConversationViewListeners() {
        for listener in conversationViewListener {
            listener.remove()
        }
    }
}

// MARK: This is the extension with the problem
extension DocumentSnapshot {
    func dataAsChat() -> Chat {
        
        // Get the reference to the messages
        let messagesRef = Firestore.firestore().collection("chats").document(self.documentID).collection("messages")
        
        // Retrieve the documents in this collection
        messagesRef.getDocuments { docs, error in
            
            var chatMessages = [ChatMessage]()
            
            if docs != nil && error == nil {
                for doc in docs!.documents {
                    chatMessages.append(doc.dataAsChatMessage())
                }
            }
            
            // Set the data to the session database
            SessionDatabase.shared.setArrayData(arrayKey: "CHAT_MESSAGES", value: chatMessages)
        }
        
        // Get the chat messages
        let messages: [ChatMessage] = SessionDatabase.shared.getArray(forKey: "CHAT_MESSAGES") as? [ChatMessage] ?? [ChatMessage]()
        
        // Create the chat
        let chat = Chat(id: self.documentID,
                        numparticipants: self["numparticipants"] as? Int ?? 0,
                        participantids: self["participantids"] as? [String] ?? [String](),
                        lastmsg: self["lastmsg"] as? String,
                        updated: self["updated"] as? Date,
                        messages: messages.isEmpty ? nil : messages)
        
        // Return the chat
        return chat
    }
}
extension QueryDocumentSnapshot {
    func dataAsChatMessage() -> ChatMessage {
        
        // Initialize the chat message object
        let chatMessage = ChatMessage(
            id: self.documentID,
            imageurl: self["imageurl"] as? String,
            msg: self["msg"] as? String ?? "",
            senderid: self["senderid"] as? String ?? "",
            timestamp: (self["timestamp"] as? Timestamp)?.dateValue())
        
        // Return the messages
        return chatMessage
    }
}
extension QueryDocumentSnapshot {
    func dataAsUser() -> User {
        
        // Initialize the user object
        var user = User()
        user.id = self.documentID
        user.firstname = self["firstname"] as? String
        user.lastname = self["lastname"] as? String
        user.phone = self["phone"] as? String
        user.photo = self ["photo"] as? String
        
        // Return the user object
        return user
    }
}

This extension works good, but the problem I’m having occurs when I need to fetch the chat messages to initialize the chat with. When fetching these chats, I receive them in a completion handler and I don’t seem to find a way to get the data out of the completion handler without losing the data. And I can’t initialize and return the chat itself in that completion handler because I can’t return something in it.

So I tried my solution to this problem was to create some kind of database that can be accessed anywhere in the app, but it only holds data for each session and then loses it.
I called this class SessionDatabase:

import Foundation
import SwiftUI

class SessionDatabase {
    
    static let shared = SessionDatabase()
    private init() {}
    
    private var database: [String : Any] = [:]
    private var arrayDatabase: [String : [Any]] = [:]
    
    /// Sets the given value to the database for the given key
    func setData(value: Any, forKey key: String) {
        database[key] = value
    }
    
    /// Returns the value in the database for the given key
    func getData(forKey key: String) -> Any? {
        return database[key]
    }
    
    /// Sets an array to the database for the given key
    func setArrayData(arrayKey key: String, value: [Any]) {
        arrayDatabase[key] = value
    }
    
    /// Returns the array for the given key
    func getArray(forKey key: String) -> [Any]? {
        return arrayDatabase[key]
    }
    
    /// Returns the value in the array for the given key at the given index
    func getArrayData(forKey key: String, atIndex index: Int) -> Any? {
        return arrayDatabase[key]?[index] ?? nil
    }
    
}

I’m working with dictionary since I thought this would be the most fitting type to use, I also implemented some function to set and get data from the dictionaries and since I needed to store an array of chat messages, I also implemented another dictionary which could hold array values.

But this solution doesn’t seem to work flawless, I don’t know why but when calling the function setData(value: Any, forKey: String) or setArrayData(arrayKey: String, value: [Any]) there isn’t any data being set to the dictionaries.

I have tested this class in a playground and it works perfectly there but not in my app…

Am I doing something wrong here or do you see some flaws in my code?
Or do I have to do it in another way?

Thanks for any help!
Rune

@Pixel

Hi Rune,

I’m a little puzzled by your paragraph where you say:

Where in the lessons does it say that there is an extension related to data(as: ?

For example, in my version, which is verbatim from Chris’s code, the getAllChats() function is as follows:

    /// This method returns all chat documents where the logged in user is a participant.
    func getAllChats(completion: @escaping ([Chat]) -> Void) {
        //  Get a reference to the database
        let db = Firestore.firestore()

        //  Perform a query against the chat collections for any chats where the user is a participant
        let chatsQuery = db.collection("chats")
            .whereField("participantids", arrayContains: AuthViewModel.getLoggedInUserId())

        let listener = chatsQuery.addSnapshotListener { snapshot, error in

            if snapshot != nil && error == nil {
                var chats = [Chat]()

                for doc in snapshot!.documents {
                    //  Parse the data into Chat structs
                    let chat = try? doc.data(as: Chat.self)

                    // Add the chat into the array
                    if let chat = chat {
                        chats.append(chat)
                    }
                }

                //  Return the data
                completion(chats)
            } else {
                print("Error in database retrieval")
            }
        }
        //  Keep track of the listener so that we can close it later.
        chatListViewListeners.append(listener)
    }

Similarly the getAllMessage() function in my version is like this:

/// This method returns all messages for a given chat
    func getAllMessages(chat: Chat, completion: @escaping ([ChatMessage]) -> Void) {

        // Check that the id is not nil
        guard chat.id != nil else {
            completion([ChatMessage]())
            return
        }

        //  Get a reference to the database
        let db = Firestore.firestore()

        //  Create the query
        let msgsQuery = db.collection("chats").document(chat.id!).collection("msgs")
            .order(by: "timestamp")

        //  Perform the query
        let listener = msgsQuery.addSnapshotListener { snapshot, error in
            if snapshot != nil && error == nil {

                //  Loop through the msg documents and create ChatMessage instances
                var messages = [ChatMessage]()

                for doc in snapshot!.documents {

                    let msg = try? doc.data(as: ChatMessage.self)

                    if let msg = msg {
                        messages.append(msg)
                    }
                }

                // Return the data
                completion(messages)
            } else {
                print("Error in database retrieval")
            }
        }
        //  Keep track of the listener so that we can close it later.
        conversationViewListeners.append(listener)
    }

Hello Chris,

Thanks for looking into my issue.

I meant the part as follows (This example is from the getAllChats() function):
let chat = try? doc.data(as: Chat.self)
I’m sorry for the misunderstanding, I thought that this was an extension.

Did the original code not work for you which is why you went down the path of creating an extension?

So, this is what’s happened so far:

I first got an error on each line of code saying that .data(as:) is removed in iOS 16.
I then built the project and noticed that not everything was working as expected, but I didn’t know the cause, so I thought I would first fix the issue with .data(as:) and then see if it had fixed anything.
So I first tried to see if I could use another similar method or extension as the .data(as:), but couldn’t find any nor the extension itself in my autocomplete.
Therefore I went down the road of creating an extension myself.

But upon reading your reply, I’ve just checked my project again and now my autocomplete is proposing the .data(as:) method again, and when I try it, I don’t get the same error saying that it is removed in iOS 16.

So I guess I don’t need my extension anymore but I’m left a little bit confused as to why I got that error.

@Pixel

Hey Rune,

At any time when you get strange errors that are related to Firebase, the first thing I do is to clean the build folder - Shift + Command + K - and then recompile. Often what happens is that if you choose the wrong option from Autocomplete, that can have an effect so cleaning the project is a first step.

Thanks for your tip Chris,

I appreciate your help!