Building a real-time chat application is a fantastic way to learn about modern mobile development techniques, especially when using powerful frameworks like SwiftUI for the user interface and Firebase Firestore for the backend. This blog post will guide you through creating a basic SwiftUI chat app that leverages Firestore’s real-time capabilities to provide a seamless chatting experience.
What You’ll Learn
By following this tutorial, you will learn:
- How to set up Firebase in your iOS project.
- How to read and write data to Firestore.
- How to display messages in a chat interface using SwiftUI.
- How to implement real-time updates using Firestore’s snapshots.
Prerequisites
Before starting, ensure you have:
- Xcode installed on your Mac.
- A Firebase project set up in the Firebase console.
- Basic knowledge of SwiftUI and Swift.
Step 1: Set Up Firebase
1. Create a Firebase Project
Go to the Firebase Console (https://console.firebase.google.com/) and create a new project.
2. Add Firebase to Your iOS App
- In the Firebase Console, select the iOS platform to add Firebase to your iOS app.
- Register your app by providing the Bundle ID.
- Download the
GoogleService-Info.plistfile and add it to your Xcode project’s root directory.
3. Install Firebase SDK
Using Swift Package Manager:
- In Xcode, go to File > Add Packages.
- Enter the Firebase GitHub repository URL:
https://github.com/firebase/firebase-ios-sdk. - Select the necessary Firebase libraries (Firestore, Authentication if you plan user authentication, etc.).
4. Initialize Firebase in Your App
In your AppDelegate.swift or entry point, import Firebase and configure it:
import FirebaseCore
import SwiftUI
class AppDelegate: NSObject, UIApplicationDelegate {
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
FirebaseApp.configure()
return true
}
}
@main
struct ChatApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Step 2: Create the Firestore Data Structure
In Firestore, you’ll need a collection to store your chat messages. Let’s call it “messages”. Each document in the “messages” collection will represent a single message with fields like text, sender, and timestamp.
Step 3: Building the SwiftUI Chat Interface
1. Define the Message Model
Create a Message struct to represent a chat message:
import FirebaseFirestore
import FirebaseFirestoreSwift
struct Message: Identifiable, Codable {
@DocumentID var id: String?
let text: String
let sender: String
let timestamp: Timestamp
// For decoding documents where 'id' might be missing (when creating new messages)
init(id: String? = nil, text: String, sender: String, timestamp: Timestamp) {
self.id = id
self.text = text
self.sender = sender
self.timestamp = timestamp
}
enum CodingKeys: String, CodingKey {
case id
case text
case sender
case timestamp
}
}
2. Create the Chat View Model
Create a ChatViewModel class to manage the messages and handle sending new messages:
import SwiftUI
import FirebaseFirestore
class ChatViewModel: ObservableObject {
@Published var messages: [Message] = []
private let db = Firestore.firestore()
init() {
fetchMessages()
}
func fetchMessages() {
db.collection("messages")
.order(by: "timestamp")
.addSnapshotListener { (querySnapshot, error) in
guard let documents = querySnapshot?.documents else {
print("No documents")
return
}
self.messages = documents.compactMap { document -> Message? in
do {
return try document.data(as: Message.self)
} catch {
print("Error decoding document: (error)")
return nil
}
}
}
}
func sendMessage(text: String, sender: String) {
let newMessage = Message(text: text, sender: sender, timestamp: Timestamp())
do {
_ = try db.collection("messages").addDocument(from: newMessage)
} catch {
print("Error saving message: (error)")
}
}
}
Here’s a breakdown of the ChatViewModel class:
- It fetches messages from Firestore and orders them by timestamp to ensure the correct order in the chat.
- It uses
addSnapshotListenerto listen for real-time updates from Firestore. - The
sendMessagefunction adds new messages to the Firestore “messages” collection with a timestamp. - Error handling is included to manage potential issues with Firestore operations.
3. Build the Chat View
Create a ChatView struct to display the chat messages and a text input for sending new messages:
import SwiftUI
struct ChatView: View {
@ObservedObject var viewModel = ChatViewModel()
@State private var newMessageText = ""
let currentUser = "User1" // Replace with actual user authentication logic
var body: some View {
VStack {
// Message List
ScrollView {
ScrollViewReader { scrollViewProxy in
LazyVStack {
ForEach(viewModel.messages) { message in
MessageView(message: message, isCurrentUser: message.sender == currentUser)
.id(message.id) // Important for scrolling to the latest message
}
}
.onReceive(viewModel.$messages) { _ in
// Scroll to the latest message when a new message is received
if let lastMessageId = viewModel.messages.last?.id {
scrollViewProxy.scrollTo(lastMessageId, anchor: .bottom)
}
}
}
}
// Message Input
HStack {
TextField("Enter message", text: $newMessageText)
.textFieldStyle(RoundedBorderTextFieldStyle())
Button(action: {
viewModel.sendMessage(text: newMessageText, sender: currentUser)
newMessageText = "" // Clear the text field
}) {
Text("Send")
}
}
.padding()
}
}
}
struct MessageView: View {
let message: Message
let isCurrentUser: Bool
var body: some View {
HStack {
if isCurrentUser {
Spacer()
Text(message.text)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 10))
} else {
Text(message.text)
.padding()
.background(Color.gray.opacity(0.5))
.clipShape(RoundedRectangle(cornerRadius: 10))
Spacer()
}
}
.padding(.horizontal)
}
}
This code snippet provides the UI elements for a simple chat interface:
- A
ScrollViewdisplays the messages, and usesScrollViewReaderto handle scrolling to the latest message. - Messages are rendered using the
MessageView, which styles messages differently based on whether they were sent by the current user. - A
TextFieldallows users to type new messages, and aButtonis used to send the message.
Step 4: Display the Chat View
Integrate ChatView into your ContentView:
import SwiftUI
struct ContentView: View {
var body: some View {
NavigationView {
ChatView()
.navigationTitle("SwiftUI Chat")
}
}
}
Conclusion
You’ve successfully built a basic SwiftUI chat application with Firestore! This application fetches and displays real-time messages. From here, you can add features like user authentication, message threading, image sharing, and more to enhance the chat experience. Firebase and SwiftUI together offer a powerful and efficient way to build modern, real-time mobile applications. Experiment and expand upon this base to create a fully featured chat application tailored to your needs!