Project 6 — Pingline / iMessage lite: the specswift-6.4/ios-26
Lesson 1 / 6
Unit 26 · Pingline

Project 6 — Pingline / iMessage lite: the spec

Note

Unit 26 · Project 6 (Pingline, an iMessage clone). Backend Tier 2 (Supabase Realtime). AI stage: Pair — the model proposes, you approve every diff. A real messaging app: 1:1 and group chats, tapback reactions, image attachments, and read receipts, all live. A mid-project AI checkpoint hunts an actor-reentrancy bug. (End-to-end encryption is deferred.)

Pingline is a messaging app: pick a conversation, see the thread, type, send, react, and watch the other side arrive live without a refresh — in one-to-one chats and in groups. Everything hard about it is concurrency you cannot see: a socket that drops and reconnects, messages that arrive out of order, a send that has to feel instant but might fail. This is the project where async Swift stops being an exercise and becomes the load-bearing wall — and where the surface grows into a real chat client on top of it.

What you build

A conversation list shows each thread — 1:1 and group — with its last message and unread badge. Tapping one opens the message thread: a scrolling transcript with your sends in blue and theirs in gray, a compose bar, and a typing indicator. Group threads attribute each message to its sender (avatar + name). Long-press a bubble for the tapback menu; a reaction shows as a badge on the bubble. Messages can carry an image attachment, and your last sent message shows a read receipt (Delivered → Read). New messages arrive live; your own appear the instant you send, then reconcile.

The screens

Screen Shows Key interaction
Conversation list 1:1 + group threads, last message, unread badge tap to open
1:1 thread bubbles, reactions, image, read receipt, typing send, react, scroll
Group thread per-sender bubbles (avatar + name), reactions send, react
Tapback menu the reaction picker over a bubble long-press, pick

What it looks like finished

Pingline conversation list — 1:1 and group threads with unread badges Pingline 1:1 thread — bubbles with a tapback, an image attachment, and a read receipt Pingline group thread — per-sender bubbles with names and reactions Pingline tapback menu — a floating reaction picker over a bubble Pingline thread with the peer's typing indicator
The finished build in the simulator: the conversation list (1:1 + group), the 1:1 thread (a tapback heart, an image bubble, a "Delivered" receipt), the group thread (per-sender attribution + reactions), the tapback menu, and the typing indicator. Rendered against seeded threads — a real build streams over Supabase Realtime through the same view model.

Concepts this locks in

  • Supabase Realtime — subscribing to database changes over a websocket.
  • AsyncSequence / AsyncStream — the incoming message feed as values you consume with for await, not a delegate callback firehose.
  • Actors for connection state — an actor ConnectionState owning connect/disconnect and the offline outbox.
  • Optimistic UI + delivery states — a sent message shown instantly, reconciled through sending → sent → delivered → read (or failed).
  • Group attribution — one thread, many senders; the row shows whose message it is.
  • Reactions & attachments — a tapback table keyed to a message; an image row with a Storage path.
  • Reentrancy discipline — the actor hazard the AI checkpoint is built around.

Backend tier: 2 (Supabase Realtime)

Messages live in a Supabase Postgres table. You INSERT a row to send, and subscribe to that table's change feed to receive; reactions and attachments are related tables. Realtime pushes every insert to every subscribed client over a websocket. You are not writing socket framing by hand — the Supabase client does that — but you are responsible for what happens when the socket is down, which is most of the real work.

AI stage: Pair

In Pair mode the model may write code, but every change arrives as a diff you read and approve. Use it to draft a subscription boilerplate or a view layout, then interrogate the diff: is this actor method safe across its await? Does this optimistic update have a rollback path?

Tip

Pair mode is not "let the model build it and skim the result." The skill is review: reading a diff for the concurrency bug that compiles cleanly and only bites under load. Approve nothing you could not defend in the rubric.

In scope

  • Live receive in 1:1 and group threads; optimistic send with delivery states.
  • Tapback reactions, image attachments, and read receipts (Delivered → Read).
  • Connection state in an actor, with reconnect and an offline send buffer.
  • Scroll-to-bottom on new messages; a typing indicator.
  • Light and dark both correct; VoiceOver on messages and the compose field.

Out of scope

  • End-to-end encryption — a real product concern, deferred past this project's lesson.
  • Message editing/deletion sync, full-history search, and pagination beyond a recent window.
  • Voice/video calls; message effects and stickers.
  • Push notifications in the graded core — the elective stretch in Milestone 3.

Definition of done

Two clients signed in as two users can hold a conversation — 1:1 and in a group — each seeing the other's messages live, their own sends instantly, reactions and attachments rendering, and read receipts advancing. Killing the network mid-conversation degrades gracefully: messages queue, the UI says so, and they flush on reconnect. Then the checkpoint, then the rubric.

Knowledge check

Q: Why is optimistic UI a defining constraint of this project, not a polish item? Because a chat that waits for a server round-trip before showing your own message feels broken. Optimistic send is the core interaction, which forces you to design reconciliation — sending → sent → delivered → read, and failure/rollback — from the start rather than bolting it on.

Q: A group thread and a 1:1 thread use the same message table. What does the group need extra? Sender attribution. Every message already has an author_id; the group thread joins it to the author's profile and renders the avatar + name above others' bubbles, where the 1:1 thread can leave it implicit. Same data, one more field surfaced.