Sending & retrieving messages

Sending messages ✉️⬆️

For retrieving messages from and sending messages to our Firebase database, we create a new Swift file called ChatController, where we create an ObservableObject. Make sure you import the Combine framework as well (we will need it later on).

import Combine
import SwiftUI

class ChatController: ObservableObject {
    
}

In our ChatController, we hold the chat messages for our ChatView. At default, meaning before we retrieved any data, the corresponding array should be empty:

class ChatController: ObservableObject {
    
    var messages = [ChatMessage]()

}

At this point, we also implement a so-called PassthroughObject, which is part of the Combine framework we just imported.

let objectWillChange = PassthroughSubject<ChatController,Never>()

Hint: The objectWillChange property needs to be assigned to a PassthroughSubject, which is part of the Combine framework. The PassthroughSubject passes its data to any view that’s observing whenever the objectWillChange property gets called. The first parameter inside the “<> ” is the data that gets passed; in our case, that should be the ChatController. The second parameter can be used for setting a rule when an error should be thrown; in our case, that should never be the case. For more details, check out this great tutorial by HackingWithSwift.

We can use the objectWillChange property to tell all observing views to rebuild their bodies manually. You’ll see how to do this in a moment. You maybe noticed that this is very similar to what the @Published property wrapper does, which we already know.

Let’s make our ChatView observe the ChatController:

@StateObject var chatController = ChatController()

We can now provide the ForEach loop of our ChatView with the messages array of our chatController instead of using the sampleConversion.

ForEach(chatController.messages, id: \.messageID) { message in
    ChatRow(message: message)
        .id(chatController.messages.firstIndex(of: message))
}
    .onAppear(perform: {
        scrollView.scrollTo(chatController.messages.count-1)
    })

When we now run the app, we don’t see any messages. That’s because our messages array is currently empty.

Let’s add a function to our ChatController to add messages to the Firebase database. This function should accept a “messageText” as an argument:

func sendMessage(messageText: String) {
    
}

When the user sends a message, we want to create a new child of our database’s “chats” node. We can automatically do this by writing:

func sendMessage(messageText: String) {
    let newChat = databaseChats.childByAutoId()
}

Next, we create a new ChatMessage instance by using the passed messageText and accessing the stored username.

func sendMessage(messageText: String) {
    let newChat = databaseChats.childByAutoId()
    let messageToSend = ["username": UserDefaults.standard.string(forKey: "username") ?? "Unknown user", "messageText": messageText]
}

Now, we can use this as our newChat’s value. 

func sendMessage(messageText: String) {
    //...
    newChat.setValue(messageToSend)
}

That’s everything we need to do to create a new ChatMessage and to add it to our Firebase realtime database! 

In our ChatView, we replace our dummy print statements and instead call this function, but only when the TextField is not empty.

TextField("New message...", text: $newMessageInput, onCommit: {
    guard !newMessageInput.isEmpty else {
        print("New message input is empty.")
        return
    }
    chatController.sendMessage(messageText: newMessageInput)
    newMessageInput = ""
})
    .padding(30)

And:

Button(action: {
    guard !newMessageInput.isEmpty else {
        print("New message input is empty.")
        return
    }
    chatController.sendMessage(messageText: newMessageInput)
    newMessageInput = ""
}) {
    Image(systemName: "paperplane")
        .imageScale(.large)
        .padding(30)
}

Awesome! Let’s run our app. Enter something into the TextField and click on the paperplane icon!

Let’s repeat this for our paperplane-Button:

Button(action: {
    guard !newMessageInput.isEmpty else {
        print("New message input is empty.")
        return
    }
    chatController.sendMessage(messageText: newMessageInput)
    newMessageInput = ""
}) {
    Image(systemName: "paperplane")
        .imageScale(.large)
        .padding(30)
}

Still, we don’t see anything because we only implemented a way to add messages to the firebase, but not to retrieve them. However, when looking into the firebase database, we see our new ChatMessage!

Retrieving messages 📩

Thus, our ChatController needs a function to retrieve the saved messages from our Firebase database.

func receiveMessages() {
    
}

Inside this function, we create a query for the last 100 messages of inside our chats node:

func receiveMessages() {
    let query = databaseChats.queryLimited(toLast: 100)
}

We can now observe our Firebase database by writing:

func receiveMessages() {
    let query = databaseChats.queryLimited(toLast: 100)
    
    _ = query.observe(.childAdded, with: { [weak self] snapshot in
        
        if  let data = snapshot.value as? [String: String],
            let retrievedUsername = data["username"],
            let retrievedMessageText = data["messageText"],
            !retrievedMessageText.isEmpty {
            let retrievedMessage = ChatMessage(messageText: retrievedMessageText, username: retrievedUsername)
            self?.messages.append(retrievedMessage)
            self?.objectWillChange.send(self!)
        }
    })
}

Every time a new ChatMessage gets added to the Firebase database, we try to fetch that message and retrieve the username and content. If the message is not empty (which shouldn’t be the case), we create a new ChatMessage instance from the retrieved data and append it to our messages array. We then call the send method of our objectWillChange property for telling the ChatView that it should rebuild its body to display the new message.

We can now call this function from our ChatView by using the .onAppear modifier. 

NavigationView {
    VStack {
        //...
    }
        .navigationTitle("Chat App")
        .onAppear {
            chatController.receiveMessages()
        }
}

When we now launch our app, the receiveMessages function gets called which retrieves the stored message(s) from our database. The messages array of our ChatController now contains that message(s) and the updated ChatView then displays it!

Try to write a new message and send it. Awesome! The sendMessage function adds a new message to the database. Our receiveMessages notices and adds it to the message array. The ChatView refreshes with eventually displaying the new message!

Tip: You can simulate a conversation by running the app on two different simulators using different usernames. If you now send a message from the one simulator, it gets displayed on the other one as well!

Conclusion 🎊

Awesome! We just finished creating our own chat app by using SwiftUI and Firebase!

By doing that, we learned how to use Firebase in SwiftUI to retrieve data from and add data to a realtime database. We also learned how to install third-party dependencies using cocoapods and saw how “NoSQL“ databases are structured.

6 replies on “Sending & retrieving messages”

Hi Andreas, I apologize if this is a duplicate post; I thought I submitted it but it doesn’t appear to be on here. Anyway, my app crashes when I run it. It will build, but when I launch I get this error message on the DatabaseConstants file:

Thread 1: “Failed to get FirebaseDatabase instance: Specify DatabaseURL within FIRApp or from your databaseForApp:URL: call.”

Not sure what to do.

Hi Jim,

I’m sorry for the late answer, the anti-spam tool hold back your comment. I apologise for that. Seems like you didn’t imported the Firebase Pods correctly. Could you please send me your Xcode project or a GitHub repo? I’ll take a look at it asap.

Hi, I had the same problem. The problem seems to be that the Database URL in the info.plist will somehow get erased from time to time.

So for me replacing the existing info plist with a new one from the firebase console solved the problem for me

Question:
– In `let objectWillChange = PassthroughSubject()`
1) What do the brackets mean?
2) What does the () brackets do? Instantiate?

Hi, the previous chapter said all errors would be cleared but since we deleted the sampleConversation array the ChatRow_Previews struct is broken.

Is there a way to simulate data like was done with CoreData?

Leave a Reply

Your email address will not be published. Required fields are marked *