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