The product depends on your phone actually ringing at a time you picked, and getting two operating systems to allow that ended up being most of the work.
Colloquial's core feature is that at the time you scheduled a practice session your phone rings, actually rings, the real incoming-call screen with a name and a ringtone, over the lock screen, whether or not the app is open. Almost everything I like about the product comes out of that one behaviour, and when I started I did not appreciate how much of the actual engineering would go on just keeping that one behaviour alive across two operating systems that both, quite reasonably, would rather a flashcard app stayed well away from their phone-call mechanic.
The incoming-call screen is not a notification you have styled to look like a call, it is the genuine telephony surface, the same one a real call arrives on, and both Apple and Google keep it behind a dedicated system, CallKit on iOS and ConnectionService on Android, because it is a powerful thing to hand an app and exactly the sort of thing you would not want one abusing. They will let you use it on the understanding that you are a voice-over-internet call, which Colloquial is, a simulated internet call that the app and the store listing both say plainly is not a real phone line. So the front door was open. It was everything on the other side of it that took the months.
The thing nobody warns you about is that the moment a call is active, iOS takes ownership of the phone's audio, the single system-wide setting for where sound comes out of and who is allowed to use the microphone, and it holds onto it firmly. And a Colloquial session, once you have answered, needs to do four audio things more or less in a row that all want a say in that same setting: it plays the recorded question, it records you saying the answer back if you have that turned on, it runs the phone's own speech recogniser to hear whether you got it right, and it plays the recorded answer. Each of those is a separate piece of the system, and each of them, when it wakes up, reaches for that one global audio setting and configures it the way it likes, and whoever touched it last wins.
This is how I lost an absurd amount of time to a bug where you would tap the speaker button, the button would light up, it would even stay lit, and the sound would keep coming quietly out of the earpiece as though you had never touched it, because a fraction of a second after your tap the recorder had started up for the next card and quietly put the audio route back where it wanted it. And a nastier cousin of the same problem, where the speech recogniser, every single time it finished listening to you, would politely hand the audio back by switching the call's session off, which on a real call meant the whole thing went silent and never came back. None of these were bugs in one place I could go and fix. They were four well-behaved components each doing the correct thing on its own and treading on each other over a resource only one of them can hold at a time, and the fix in the end was less "find the broken line" and more "decide, on every single transition, who is allowed to own the sound right now, and then re-assert that, fully, every time".
Here is the part that made the iOS side slow, as opposed to merely hard. CallKit does not run on the simulator. Not "runs with some limitations", it is simply not there, so a practice call cannot happen on a simulator at all, which means every one of those audio-ownership bugs could only be reproduced. Every attempted fix could only be tested, on an actual iPhone, on an actual call, through the full loop of building onto the device, placing a real call, answering it and listening. When the symptom is "the third card comes out of the earpiece instead of the speaker", there is no unit test that catches it and no emulator that shows it, there is only me, a phone, and a session I have now sat through more times than I would have liked. I got very familiar with my own flashcards.
Android, to its credit, let me answer the ringing phone in an emulator, so I could prove the whole ring-and-answer path there without a physical device in hand, and that alone made it the calmer of the two platforms to build on. It was not without its own sharp edges. The ring has to punch through the lock screen, which is its own permission and its own kind of full-screen intent, and it is deliberately awkward to do, because that surface is valuable for the same reason the iOS one is guarded. And Android keeps tightening the rules around long-running background work with each version, sensibly, so at one point I narrowed a single line in a manifest to ask for less than I had before, which is exactly the direction you are meant to move in, and it turned a working call into a hard crash at the precise moment you hit answer, because a piece of the calling mechanic underneath was still asking for the broader permission I had just taken away, and the newer Android now checks that the two agree. That one only showed up on a real release build too, never on the debug build I run day to day, which is its own recurring lesson.
And all of that is only the part that happens on the phone. There is a whole other half, on the server, whose only job is to set the ring going at the second you asked for it, and it has been the source of some of the quietest and most alarming failures, quiet because when a call simply does not arrive there is nothing on your screen to see, no error, no spinner, just a session that never happened. Early on I had the server set to shut itself down when idle to save money, which is the right instinct for an ordinary web app and completely wrong for this one, because a machine that is asleep has no idea a call is due, and a real scheduled session once sat five minutes overdue and only rang at all because an unrelated request happened to wake the machine up. And later, one dead network connection to Apple's push servers, the kind that drops without sending the little goodbye packet that would tell you it had dropped, quietly swallowed three consecutive morning rings before I worked out that the connection I thought I was sending through had been a corpse for the better part of a day, which is why it now gets a heartbeat and gets replaced on a timer so it can never rot like that again.
I could have made every one of these problems disappear by making it a notification instead. No telephony surface to borrow, no audio session to wrestle over, no physical-device testing loop, no server that has to be awake at the exact right minute. It would have been a fraction of the work and it would have been a quietly worse product, because the whole reason the thing works, the reason a scheduled session actually pulls you in rather than getting swiped away with everything else, is that it arrives as a call and not a reminder. The ring is the product itself and not a flourish laid over the top of it, which is the whole reason those months of arguing with two operating systems for the right to make your phone ring were not a tax on the real work but the real work.
And in case that sounds like I have it all solved: the most recent one, just so you know I am not pretending this is finished, was the permission screen that comes up right after you sign up, which for a while had a bug where the buttons looked completely normal and simply did not respond to being tapped, on the single most common way people actually sign up, the sort of thing that stays invisible until a real person on a real phone tries it and tells you. Fixed now. There will be another.
Colloquial is a flashcard app that calls you and walks you through your deck out loud.
See how it works →