Sitemap
Codelps

Codelps

🚀 We launch your app in 28 days 📱 iOS • Android • Web • AI • SaaS 🏆 100+ Apps Shipped | MVP → Scale 📞 +1 (415) 740-0013 📅 Book a free call ↓

How I Built a Messenger-Style Typing Indicator in iOS (Swift + Sockets)

5 min readSep 28, 2026

--

A real production case study: socket events, NotificationCenter, and an animated UITableViewCell that doesn’t leak.

Press enter or click to view image in full size

A guest opens the chat, asks about a car, and then… nothing. The admin on the other side has no idea if the guest is writing a long question or has already left the app.

That was the situation in a car rental admin app we were building at Codelps. The server was already emitting typing:start and typing:stop events. The iOS app just wasn't listening. So we built a Messenger-style typing indicator, the three pulsing dots you've seen a thousand times, and wired it end to end.

It looks like a small feature. It isn’t. Here’s exactly how we did it, and the mistakes worth avoiding.

The Problem

We had three issues to solve:

  • No visual feedback when the guest was typing.
  • Socket events existed on the server but weren’t consumed anywhere in the UI.
  • The typing row had to live alongside real message rows and hide itself automatically.

That last one is where most implementations break. A typing indicator that gets stuck on screen is worse than no indicator at all.

The Socket Flow

Before writing any UI, we mapped the full flow:

  1. The peer types, and the server emits typing:start.
  2. ChatSocketManager posts a .SOCKET_TYPING_START notification.
  3. ChatVC sets isPeerTyping = true and inserts a TypingIndicatorTableViewCell.
  4. When the peer stops typing or sends a message, the typing row is removed.
  5. When the local admin types, the app emits typing:start on text change and typing:stop on send.

Why NotificationCenter? It keeps the socket layer completely unaware of view controllers. The manager broadcasts, and whichever screen cares can listen.

swift

// ChatSocketManager.swift
socket.on("typing:start") { _, _ in
print("ChatSocket: typing:start received")
DispatchQueue.main.async {
NotificationCenter.default.post(name: .SOCKET_TYPING_START, object: nil)
}
}
socket.on("typing:stop") { _, _ in
print("ChatSocket: typing:stop received")
DispatchQueue.main.async {
NotificationCenter.default.post(name: .SOCKET_TYPING_STOP, object: nil)
}
}

Notice the DispatchQueue.main.async. Socket callbacks often arrive on a background thread, and touching UITableView from there is one of the fastest ways to get random crashes.

Building the Typing Cell

We built TypingIndicatorTableViewCell fully in code, no XIB. It has a bubble, two small tail views (the "thought bubble" look), and three dots.

The animation is a CABasicAnimation on opacity, with a staggered beginTime for each dot. That stagger is what creates the wave effect.

swift

final class TypingIndicatorTableViewCell: UITableViewCell {
    static let reuseId = "TypingIndicatorTableViewCell"    private let bubble = UIView()
private var dots: [UIView] = []
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
selectionStyle = .none
backgroundColor = .clear
setupBubble() // bubble + two tail views
setupDots() // three 8pt circles inside the bubble
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func startAnimatingDots() {
for (index, dot) in dots.enumerated() {
let pulse = CABasicAnimation(keyPath: "opacity")
pulse.fromValue = 0.3
pulse.toValue = 1.0
pulse.duration = 0.6
pulse.autoreverses = true
pulse.repeatCount = .infinity
pulse.beginTime = CACurrentMediaTime() + Double(index) * 0.2
pulse.fillMode = .backwards
dot.layer.add(pulse, forKey: "typingPulse")
}
}
func stopAnimatingDots() {
dots.forEach { $0.layer.removeAnimation(forKey: "typingPulse") }
}
override func prepareForReuse() {
super.prepareForReuse()
stopAnimatingDots()
}
}

Two details matter here. First, the bubble uses the same incoming-message color as real guest messages (Colors.BUBBLE_INCOMING_COLOR), so it feels native to the thread. Second, prepareForReuse calls stopAnimatingDots(). Skip that and you'll see "ghost" animations on recycled cells while scrolling.

Integrating It into the Chat Screen

Inside ChatVC, the typing indicator is simply one extra row at the bottom of the table when isPeerTyping is true.

Get Codelps’s stories in your inbox

Join Medium for free to get updates from this writer.

swift

private var isPeerTyping = false
private var peerTypingHideWorkItem: DispatchWorkItem?
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
messages.count + (isPeerTyping ? 1 : 0)
}

When a typing:start notification arrives, we show the row and schedule an auto-hide. Every new event cancels the previous timer, so the dots stay visible as long as the guest keeps typing.

swift

@objc private func peerStartedTyping() {
peerTypingHideWorkItem?.cancel()
    if !isPeerTyping {
isPeerTyping = true
let indexPath = IndexPath(row: messages.count, section: 0)
tableView.insertRows(at: [indexPath], with: .fade)
if isNearBottom { scrollToBottom() }
}
let hide = DispatchWorkItem { [weak self] in self?.hideTypingIndicator() }
peerTypingHideWorkItem = hide
DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: hide)
}

We only scroll to the bottom if the admin is already near the bottom. Yanking someone away from older messages they’re reading is annoying.

The final rule: when a real message arrives, the typing row goes away first. We handle that inside appendIncomingMessage, so there's never a moment where the dots and the new message sit on screen together.

The Timeout Fallback (Don’t Skip This)

Here’s the lesson that bit us: you can’t trust typing:stop to always arrive. The guest's network drops. They kill the app. They lock their phone mid-sentence.

If your UI only hides the indicator on typing:stop, it will eventually get stuck. The peerTypingHideWorkItem timeout is the safety net. Whatever happens on the network, the dots disappear on their own.

Testing Checklist

Before calling it done, we ran through this on a real device, not just the simulator:

  • Guest types → dots appear in the admin thread.
  • Guest sends a message → dots disappear and the message shows.
  • Guest stops typing → dots disappear after the timeout.
  • Scrolling and cell reuse → no ghost animations.

Common Mistakes to Avoid

These showed up either in this feature or in related chat work on the same project:

  • Updating UITableView from a background thread. Always hop to the main queue.
  • Forgetting prepareForReuse cleanup on animated cells.
  • Deduplicating chat messages by text instead of the server ID. Two guests can type “ok”.
  • Assuming one payload shape forever. Backends change. Keep parsing in the manager layer.
  • Changing socket, UI, and push logic in one commit. Fix one layer at a time.

Key Takeaways

Typing indicators need three things to feel right: a clean event pipeline, a cell that cleans up after itself, and a timeout that never lets the UI lie. And always hide the typing row the moment a real message lands.

With a write-up like this, a junior developer can ship the same feature in 4–6 hours instead of 1–2 days. That’s why we document every non-trivial feature at Codelps.

I’m Hafiz Anser, founder of Codelps, a Lahore-based studio that has shipped 100+ mobile apps for US founders. If you’re building a product with real-time chat and need an iOS team that sweats these details, reach out at info@codelps.com or visit codelps.com.

--

--

Codelps
Codelps