Swadesh is an iPhone app that reminds me to call family and friends. I set how often I want to speak to each person, and it tracks who I have fallen behind on.
It is deliberately small. There is no feed, no streaks and no social layer. It answers one question: who have I not spoken to in too long.
I moved from Dhaka to Pennsylvania and staying in touch got harder than I expected. I wanted something that would tell me who I had not spoken to in a while.
The apps I found all wanted an account and a copy of my contacts. I did not want that list living on someone else's server, so I wrote my own.
Every contact has a target: weekly for one person, monthly for another. The ranking is the time elapsed divided by that person's own target, so the list compares people on different cadences fairly.
Because the ranking depends on what time it is, it cannot be a stored sort. It is computed in memory against a single reference date that ticks once a minute and refreshes when the app comes back to the foreground, so every card on screen agrees on what now means.
This is the part I spent the most time on. A reminder is useless if it wakes you, and worse if it reaches you when the person you are meant to call is asleep. So the notification time is a search, not a fixed hour.
The search starts at 10:00 on the due day and walks forward hour by hour until it finds a slot inside the 9am to 9pm window for both people. It compares minutes of the day, so time zones offset by thirty or forty five minutes work correctly. It never returns a time in the past, so someone already overdue gets reminded today instead of never.
Two twelve hour windows on a twenty four hour circle always intersect, with exactly one exception: an offset of precisely twelve hours, where they are perfect complements. That single case is flagged and resolved in the recipient's favour, since a reminder has to reach someone who is awake.
Marking someone as contacted is the one piece of state that matters, so the app only writes it once iOS confirms it actually opened WhatsApp or the dialler.
The call path throws before touching the model if the URL cannot be opened, so a failed hand-off never records a conversation that did not happen. The URL opener is injected rather than hard-wired, which is what makes that path testable without leaving the app.
Phone numbers are stored exactly as you typed them and sanitised only on the way out. The formatter folds non-ASCII digits, including Bengali and Arabic-Indic numerals, rewrites a leading double zero to a plus, and returns nothing at all rather than a bare country code when there is no dialable number left.
An iOS notification trigger fires once and is then consumed. Ignore a reminder and nothing brings it back, which would leave the app permanently silent about exactly the people it exists to nag me about.
So a reconciliation pass runs on launch and on every return to the foreground. It rebuilds pending reminders, and treats a trigger pinned to a stale time zone as expired, so travelling re-resolves the calling window. Each contact carries an explicit identifier that doubles as the notification identifier, so re-arming replaces the old request instead of stacking duplicates.
There is no account, no server and no sync. The contact list, the targets and the history live in a local database on the phone and never leave it.
In daily use. Contacts, targets, the dashboard, notifications and importing from the iOS address book are working. The cadence and sorting logic has no UI or database dependencies, so it runs under a test script without Xcode, which is how I check it.