Building the In-Game Phone

The central design constraint for Detective Aloha was this: everything happens on your real phone, through interfaces that feel like the ones you already use. No game menus. No dialogue wheels. Just texts, a case file app, and a photos library.

That meant building an iMessage-style chat interface in SwiftUI that feels real enough that the fiction holds.

The illusion of a real conversation

The hard part isn't rendering chat bubbles — that's table stakes. The hard part is timing. A real text conversation has rhythm: the other person is typing, they pause, they send in fragments.

I use a small state machine per contact. When you send a message, the suspect enters a "composing" state with a randomized delay drawn from a distribution tuned to their personality. Nervous characters respond fast. Evasive ones make you wait.

func scheduleReply(for contact: Contact, after delay: TimeInterval) {
    DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
        self?.deliverNextMessage(for: contact)
    }
}

The Case File

The Case File is a corkboard-style view where observations get pinned as you unlock them. Each card is a piece of testimony or evidence. The player connects the dots manually — there's no automated deduction engine.

I wanted the Case File to feel like physical evidence: things you can arrange, cross-reference, and sit with. SwiftUI's ZStack with drag gesture tracking turned out to be the right primitive.

What didn't work

My first version used a custom scroll container for the chat view. It had subtle bugs with keyboard avoidance that took three weeks to track down. I eventually switched to ScrollViewReader with a LazyVStack and the problem went away.

If you're building a chat UI in SwiftUI: use the built-in scroll infrastructure, not a custom container. The system knows things about the keyboard that you don't.