Swift and SwiftUI tutorials for Swift Developers

How to build a WhatsApp Clone in SwiftUI

If you are an iOS Developer looking to take your skills to the next level, there is no better project for your portfolio than building real-world applications. Today we will dive into one of the most complete and rewarding challenges: creating a WhatsApp Clone in SwiftUI.

Swift programming has evolved drastically in recent years, and with the maturity of SwiftUI, we can now build fluid, reactive, and cross-platform user interfaces with a fraction of the code we used to need in UIKit. In this massive step-by-step tutorial, you will learn how to use Swift and the Xcode development environment to build a functional messaging application that not only runs on the iPhone, but also shares code to run natively on iPadOS, macOS, and watchOS.


1. Prerequisites for the iOS Developer

Before writing our first line of code in Swift, make sure you have:

  • Xcode 15 or higher (required for the latest SwiftUI features).
  • Intermediate knowledge of Swift programming.
  • Familiarity with SwiftUI basics (VStack, HStack, List, NavigationStack).
  • A spirit of continuous learning, the most important quality of any iOS Developer.

2. Step 1: Cross-Platform Project Setup in Xcode

The true power of SwiftUI lies in its “Learn once, apply anywhere” capability. Let’s set up our project to support the entire Apple ecosystem.

  1. Open Xcode and select Create a new Xcode project.
  2. In the template selection tab, go to the Multiplatform section and select App. This will set up our Swift project to share the codebase between iOS and macOS.
  3. Name your project: WhatsAppClone.
  4. Make sure Interface is set to SwiftUI and Language to Swift.
  5. For watchOS, we will go to File > New > Target, select the watchOS tab, and add an App Target to our existing project.

Congratulations! You now have the foundation for your WhatsApp Clone in SwiftUI.


3. Step 2: Data Modeling and Business Logic (Swift Programming)

A good iOS Developer knows that the interface is just the tip of the iceberg. We need a solid database. We will create data models using the modern features of Swift programming.

Create a new file named Models.swift.

import Foundation

// User Model
struct User: Identifiable, Hashable {
    let id: UUID
    let name: String
    let profileImageName: String
    let status: String
}

// Message Model
struct Message: Identifiable, Hashable {
    let id = UUID()
    let text: String
    let timestamp: Date
    let isCurrentUser: Bool
}

// Chat Model
struct Chat: Identifiable, Hashable {
    let id = UUID()
    let participant: User
    var messages: [Message]
    var unreadCount: Int
    
    var lastMessage: Message? {
        messages.last
    }
}

These models are lightweight and conform to the Identifiable protocol, making them perfect for iterating over in SwiftUI lists.

Next, we will create our ViewModel. In the MVVM paradigm, this file will handle our app’s state. Create ChatViewModel.swift:

import Foundation
import Combine

class ChatViewModel: ObservableObject {
    @Published var chats: [Chat] = []
    
    init() {
        loadMockData()
    }
    
    private func loadMockData() {
        // Mock data for our WhatsApp Clone in SwiftUI
        let user1 = User(id: UUID(), name: "Elena Gomez", profileImageName: "person.circle.fill", status: "Available")
        let user2 = User(id: UUID(), name: "Carlos Dev", profileImageName: "person.circle", status: "At work")
        
        let msg1 = Message(text: "Hi! Did you see the new Xcode update?", timestamp: Date().addingTimeInterval(-3600), isCurrentUser: false)
        let msg2 = Message(text: "Yes, SwiftUI is amazing this year.", timestamp: Date().addingTimeInterval(-1800), isCurrentUser: true)
        
        let chat1 = Chat(participant: user1, messages: [msg1, msg2], unreadCount: 0)
        let chat2 = Chat(participant: user2, messages: [Message(text: "Are we uploading the app to TestFlight today?", timestamp: Date(), isCurrentUser: false)], unreadCount: 1)
        
        self.chats = [chat1, chat2]
    }
    
    func sendMessage(_ text: String, to chatID: UUID) {
        if let index = chats.firstIndex(where: { $0.id == chatID }) {
            let newMessage = Message(text: text, timestamp: Date(), isCurrentUser: true)
            chats[index].messages.append(newMessage)
        }
    }
}

4. Step 3: Building the iOS Interface with SwiftUI

Now comes the visual part. In our WhatsApp Clone in SwiftUI, we’ll start by building the main chat list view, the screen you see right when you open the app.

The Chat Row View (ChatRowView)

This view will represent each cell in our chat list. Create a file ChatRowView.swift:

import SwiftUI

struct ChatRowView: View {
    let chat: Chat
    
    var body: some View {
        HStack(spacing: 15) {
            Image(systemName: chat.participant.profileImageName)
                .resizable()
                .scaledToFill()
                .frame(width: 50, height: 50)
                .foregroundColor(.gray)
                .clipShape(Circle())
            
            VStack(alignment: .leading, spacing: 5) {
                HStack {
                    Text(chat.participant.name)
                        .font(.headline)
                        .fontWeight(.semibold)
                    
                    Spacer()
                    
                    if let lastMsg = chat.lastMessage {
                        Text(lastMsg.timestamp, style: .time)
                            .font(.caption)
                            .foregroundColor(.gray)
                    }
                }
                
                HStack {
                    if let lastMsg = chat.lastMessage {
                        Text(lastMsg.text)
                            .font(.subheadline)
                            .foregroundColor(.gray)
                            .lineLimit(1)
                    }
                    
                    Spacer()
                    
                    if chat.unreadCount > 0 {
                        Text("\(chat.unreadCount)")
                            .font(.caption2)
                            .padding(6)
                            .foregroundColor(.white)
                            .background(Color.blue)
                            .clipShape(Circle())
                    }
                }
            }
        }
        .padding(.vertical, 4)
    }
}

The Main Chat List (ChatListView)

Taking advantage of SwiftUI‘s NavigationStack (or NavigationView in older versions, though every good iOS Developer should stay updated), we’ll create the main structure of the iOS app.

import SwiftUI

struct ChatListView: View {
    @StateObject private var viewModel = ChatViewModel()
    
    var body: some View {
        NavigationStack {
            List(viewModel.chats) { chat in
                NavigationLink(value: chat) {
                    ChatRowView(chat: chat)
                }
            }
            .listStyle(PlainListStyle())
            .navigationTitle("Chats")
            .navigationDestination(for: Chat.self) { chat in
                ChatDetailView(viewModel: viewModel, chat: chat)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button(action: { /* New conversation */ }) {
                        Image(systemName: "square.and.pencil")
                    }
                }
            }
        }
    }
}

The Conversation View (ChatDetailView)

For our WhatsApp Clone in SwiftUI to feel real, we need an immersive messaging screen. Using ScrollViewReader is crucial here to automatically scroll to the last sent message, an essential trick of Swift programming applied to UIs.

import SwiftUI

struct ChatDetailView: View {
    @ObservedObject var viewModel: ChatViewModel
    var chat: Chat
    @State private var messageText = ""
    
    // Get the updated chat from the ViewModel
    var currentChat: Chat? {
        viewModel.chats.first(where: { $0.id == chat.id })
    }
    
    var body: some View {
        VStack {
            ScrollView {
                ScrollViewReader { proxy in
                    VStack(spacing: 15) {
                        ForEach(currentChat?.messages ?? []) { message in
                            MessageBubbleView(message: message)
                                .id(message.id)
                        }
                    }
                    .padding()
                    .onChange(of: currentChat?.messages.count) { _ in
                        // Auto-scroll to the last message
                        if let lastID = currentChat?.messages.last?.id {
                            withAnimation {
                                proxy.scrollTo(lastID, anchor: .bottom)
                            }
                        }
                    }
                }
            }
            
            // Text Input Area
            HStack {
                Button(action: {}) {
                    Image(systemName: "plus")
                        .font(.title2)
                        .foregroundColor(.blue)
                }
                
                TextField("Type a message...", text: $messageText)
                    .padding(10)
                    .background(Color(.systemGray6))
                    .cornerRadius(20)
                
                Button(action: sendMessage) {
                    Image(systemName: messageText.isEmpty ? "mic" : "paperplane.fill")
                        .font(.title2)
                        .foregroundColor(messageText.isEmpty ? .gray : .blue)
                }
            }
            .padding()
            .background(Color(.systemBackground))
        }
        .navigationTitle(chat.participant.name)
        .navigationBarTitleDisplayMode(.inline)
    }
    
    private func sendMessage() {
        guard !messageText.trimmingCharacters(in: .whitespaces).isEmpty else { return }
        viewModel.sendMessage(messageText, to: chat.id)
        messageText = ""
    }
}

// Subview for chat bubbles
struct MessageBubbleView: View {
    let message: Message
    
    var body: some View {
        HStack {
            if message.isCurrentUser { Spacer() }
            
            Text(message.text)
                .padding(12)
                .background(message.isCurrentUser ? Color.blue : Color(.systemGray5))
                .foregroundColor(message.isCurrentUser ? .white : .primary)
                .cornerRadius(16)
                .frame(maxWidth: 250, alignment: message.isCurrentUser ? .trailing : .leading)
            
            if !message.isCurrentUser { Spacer() }
        }
    }
}

5. Step 4: Taking our Clone to macOS in Xcode

Since you created a Multiplatform project in Xcode, the previous SwiftUI code will already compile on macOS. However, a professional iOS Developer knows that the user experience (UX) on desktop is different from mobile.

On iOS we use NavigationStack because navigation is hierarchical (forward and back). On macOS and iPadOS, it is ideal to use NavigationSplitView to show the chat list in a sidebar and the active conversation in the main panel.

We will modify our entry point (App.swift or a main ContentView) to use conditional compilation or take advantage of SwiftUI‘s adaptability:

import SwiftUI

@main
struct WhatsAppCloneApp: App {
    @StateObject private var viewModel = ChatViewModel()
    
    var body: some Scene {
        WindowGroup {
            #if os(macOS) || targetEnvironment(macCatalyst)
            MacNavigationSplitView(viewModel: viewModel)
            #else
            ChatListView() // The iOS view we already created
            #endif
        }
    }
}

// Specific view for macOS/iPadOS
struct MacNavigationSplitView: View {
    @ObservedObject var viewModel: ChatViewModel
    @State private var selectedChat: Chat?
    
    var body: some View {
        NavigationSplitView {
            List(viewModel.chats, selection: $selectedChat) { chat in
                NavigationLink(value: chat) {
                    ChatRowView(chat: chat)
                }
            }
            .navigationTitle("Chats")
            .frame(minWidth: 250)
        } detail: {
            if let chat = selectedChat {
                ChatDetailView(viewModel: viewModel, chat: chat)
            } else {
                Text("Select a chat to start messaging")
                    .foregroundColor(.gray)
            }
        }
    }
}

With just a few lines of Swift programming, we have transformed our mobile interface into a desktop app with adaptive panels. That is the magic of Xcode and the Apple ecosystem!


6. Step 5: Expanding to watchOS

The Apple Watch requires an even more simplified design. The screen is small, so elements like the search bar or complex text inputs must be handled with care. As an iOS Developer, you must design for the user’s context.

In your watchOS Target in Xcode, we’ll create a specialized interface. We will reuse the same model (Models.swift) and logic (ChatViewModel.swift) files, but we will create separate views for the watch.

import SwiftUI

// WatchContentView.swift (watchOS Target)
struct WatchContentView: View {
    @StateObject private var viewModel = ChatViewModel()
    
    var body: some View {
        NavigationStack {
            List(viewModel.chats) { chat in
                NavigationLink(value: chat) {
                    VStack(alignment: .leading) {
                        Text(chat.participant.name)
                            .font(.headline)
                        Text(chat.lastMessage?.text ?? "")
                            .font(.caption)
                            .foregroundColor(.gray)
                            .lineLimit(1)
                    }
                }
            }
            .navigationTitle("Messages")
            .navigationDestination(for: Chat.self) { chat in
                WatchChatDetailView(viewModel: viewModel, chat: chat)
            }
        }
    }
}

struct WatchChatDetailView: View {
    @ObservedObject var viewModel: ChatViewModel
    var chat: Chat
    
    var currentChat: Chat? {
        viewModel.chats.first(where: { $0.id == chat.id })
    }
    
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 8) {
                ForEach(currentChat?.messages ?? []) { message in
                    Text(message.text)
                        .padding(8)
                        .background(message.isCurrentUser ? Color.blue : Color.gray.opacity(0.3))
                        .cornerRadius(10)
                        .frame(maxWidth: .infinity, alignment: message.isCurrentUser ? .trailing : .leading)
                }
            }
            .padding(.horizontal)
        }
        .navigationTitle(chat.participant.name)
    }
}

Notice how we removed the profile picture in the chat list to save vertical space, a fundamental design decision when programming for the wrist.


7. Advanced Swift Programming Techniques to Optimize Your Clone

If you want this WhatsApp Clone in SwiftUI to stand out in your portfolio against other developers, you must implement good practices that recruiters look for in a Senior iOS Developer:

1. Dependency Injection

Instead of initializing ChatViewModel directly inside views, inject it using the environment (@EnvironmentObject). This makes your Swift code more modular and much easier to test (Unit Testing).

2. Using Swift Concurrency (async/await)

If you were to connect this app to a real backend (for example, Firebase or Supabase), you should replace the mock data loading (loadMockData()) with asynchronous network calls using async/await. Modern Swift programming abandons traditional closures in favor of cleaner concurrent code that is safe against race conditions.

3. Accessibility

An application built with SwiftUI is only as good as its accessibility. Add modifiers like .accessibilityLabel("Message from \(chat.participant.name)") to your cells. In Xcode, you can use the Accessibility Inspector to verify that VoiceOver reads the interface correctly.

4. Local Persistence with SwiftData or CoreData

A real WhatsApp clone saves messages locally so you can read them offline. Implementing SwiftData (the native persistence framework recently introduced by Apple) in your model will turn your app from a simple prototype into a production-grade application.


Conclusion

Creating a WhatsApp Clone in SwiftUI is a masterclass in architecture, UI design, and Swift programming. Throughout this tutorial, we have seen how to configure Xcode for cross-platform development, how to structure data using MVVM, and how to adapt a single business logic to three completely different platforms: iPhone (iOS), Mac (macOS), and Apple Watch (watchOS).

Being a great iOS Developer isn’t just about knowing Swift syntax, but about understanding how to solve complex problems using the tools Apple provides. SwiftUI has shown us that writing fluid native interfaces is today more intuitive and faster than ever.

Leave a Reply

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

Previous Article

How to Get View Size in SwiftUI

Related Posts