Swadesh

What it is

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.

Why I built it

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.

Who comes first

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.

Ranked by elapsed ÷ target, not by date Amma every 7 days 21 days ago 3.00 overdue Nanu every 14 days 16 days ago 1.14 overdue Bhai every 30 days 24 days ago 0.80 due soon Rafi every 7 days 2 days ago 0.29 on track 1.0 = due Amma is on a weekly cadence and Bhai a monthly one, so the raw gap would rank them wrongly.
Figure 1. Who is most overdue, measured against each person’s own target.

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.

When the reminder arrives

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.

Finding an hour that is daytime at both ends 00 03 06 09 12 15 18 21 00 your local clock You State College awake 09:00 – 21:00 Amma Dhaka, +10h awake 09:00 – 21:00 in Dhaka overlap reminder at 10:00 for you, 20:00 for her
Figure 2. A reminder lands only where both waking windows overlap.

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.

Logging a call honestly

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.

Tap a contact WhatsApp or call Build the number sanitised on the way out iOS opens it? canOpenURL, then open Yes log the call No nothing written the reminder is then re-armed A failed hand-off never fakes a conversation, so the app cannot quietly lose track of someone.
Figure 3. A call is recorded only after iOS confirms the hand-off.

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.

Reminders that re-arm themselves

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.

Where it all runs

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.

Your iPhone Dashboard cards, sorted by urgency ContactNode SwiftData, on disk CallManager deep links, logging NotificationManager one reminder per contact ReminderTimeResolver No account No server No sync nothing leaves the device
Figure 4. Everything runs on the phone.

Status

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.

Type
Personal project, in progress
Stack
Swift, SwiftUI, SwiftData, iOS 17
Data
On device only, no network calls

Related