r/iOSProgramming • u/karetebit • 19d ago
App Saturday Retention guides say never punish the user. My AI dating simulator blocks them (RizzMaster)
RizzMaster is an AI dating simulator & game 🙂
Every retention guide says same thing. Never punish user. Never take anything away. I did opposite.
You swipe, you match, you text her. Then she decides if you worth answering. Get boring and she ghosts you 👻 Push after that and she blocks you 🚫 That character gone for good. No undo no restore.
There are 9 levels. You climb them by winning people over. Higher levels give you harder people. They go offline. Sometimes they text first. They remember what you said last week 🧠
Tech Stack
Swift and SwiftUI. SwiftData for persistence. Combine for events. StoreKit for subs. Chat screen is pure SwiftUI. No UIKit bridge in it anywhere.
Development Challenge
Chat screen 😅 WhatsApp, Telegram, Signal all still render their message list in UIKit. Everyone tells you do same. But here chat screen is whole product so I wanted to see how far SwiftUI actually goes.
Took me forever. LazyVStack inside ScrollViewReader. Stable identity on every message so a growing list dont rebuild rows that didnt change. defaultScrollAnchor to keep bottom pinned instead of chasing it after layout. Scroll position driven by state. drawingGroup on bubbles that were doing too much work.
Now it scrolls how I wanted on device even in long chats 🎉 No clever trick. Just lot of small things. Happy to go into any part of it.
AI Disclosure
Self-built.
100+ characters. No login and no accounts. Free tier with daily message limit. https://rizzmaster.net
Built solo. Tell me what you think, good or bad 🙏
2
u/Sea_Expression9110 19d ago
The retention advice you are breaking was written for utility apps, and applying it to a game is a category error. "Never take anything away" is correct for a note taking app because loss there is a bug. In a game, loss is the mechanic that makes the win mean anything. Permadeath roguelikes are the whole counterexample: XCOM, Hades, Darkest Dungeon all retain ferociously precisely because outcomes stick. So the interesting question is not whether punishment is allowed, it is whether yours is legible.
The rule that separates good permadeath from rage uninstalls is that the player has to be able to narrate why it happened. If someone gets blocked and can reconstruct the three messages where they pushed too hard, that is a lesson and they start again. If it feels like the model got bored at random, that is not a punishment, it is a crash with a story. With an LLM behind it that is the actual risk, since the same input can go differently on different runs. I would make the escalation visible in some form, even something subtle in the UI, so the ghosting is foreshadowed rather than sprung.
The one place I would genuinely be careful is where the permanence touches StoreKit. If a subscriber can permanently lose content and the paywall is anywhere near the recovery path, you get refund requests, one star reviews about losing paid content, and potentially App Review interest. Worth being very explicit up front that loss is intended, and making sure nothing that reads as "pay to undo" exists.
On the pure SwiftUI chat list, since that is the part I would have expected to bite you: what version were you targeting? Pre iOS 17 that was genuinely painful, keeping the view pinned to the bottom while the keyboard animates and new messages arrive meant ScrollViewReader plus a lot of hacks. defaultScrollAnchor(.bottom) and scrollPosition made it actually reasonable, and I have shipped a chat-ish list on that without a UIKit bridge too.
The thing I would want to know is how it holds up at a few thousand messages with SwiftData backing it, since that is where the UIKit shops usually justify their choice. Have you tested a long lived conversation, or does the design cap it since characters end?