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.