Simple Changes That Save Time in App Development
In product development, I keep noticing how much time disappears into things that are technically possible, but unnecessarily awkward.
Small bits of friction accumulate: hidden debug entry points, manual typing, version-number ambiguity, release documents that need rework, and verification steps that take longer than they should.
Programming is especially good at removing this kind of friction. That is part of its nature.
I have automated many internal workflow steps with skills and tooling, but this post is about a narrower set of changes: simple product or release-facing improvements that had real impact on the people working around them.
The principle
When a team keeps paying the same small cost, I usually try to ask:
- Is this step actually necessary?
- If it is necessary, can the product surface it more directly?
- If not, can we remove the step entirely instead of documenting it better?
That led to a handful of changes that were individually small, but collectively meaningful.
1. Showing analytics logs directly in the Xcode terminal
One repeated pain point was verifying analytics or user-behavior logs during development. The immediate trigger was an old team error that roughly meant: B was being processed before A was ready.
When I analyze an error, I start with reproduction, not imagination. I try to get as close as possible to the failing environment and compare it with the real error case. In this case, exact reproduction was difficult, but the existing error logs were already quite detailed because logs had been inserted in multiple places over time.
That made comparison the most useful path. I wanted to line up the stored logs from the real error case against the logs from a normal run and inspect them side by side. So I changed the setup so the relevant logs appeared directly in the Xcode terminal, which made that comparison much easier.
I also expanded the logging points beyond user events alone to include API requests and relevant feature flag values. That way, when analyzing an issue, I could inspect user behavior, configuration, and network context in one place instead of jumping across separate tools or websites.
I also did not want this to become noisy or slow for everyone else. On heavier screens such as the home screen, logging every impression can create noticeable delay. So I added a DebugLogger that routes these messages through Logger(subsystem:category:).debug only in debug builds. Since that flow already follows OS_ACTIVITY_MODE, controlled through a scheme environment variable, the logs can stay disabled by default and be enabled only during active investigation.
This did not change the product for end users, but it made product work faster and more reliable. We did have other logging tools with nicer UI, but for this kind of analysis, plain text is often more accessible because it is easier to scan and compare line by line. That is also why I do not like fixing bugs from intuition alone: without evidence, it is hard to know whether we solved the real problem or only changed the code around it.
2. Replacing a typed debug keyword with device shake
We also had a server-environment switching entry point that relied on typing a specific keyword.
That worked, but only if you:
- knew the keyword
- remembered the exact spelling
- were willing to type it every time
That is not a great interface for something internal teams need repeatedly.
So I made the same entry point available through device shake as well.
This is a small example, but I like it because it reflects a broader lesson: when a path is meant to be used by humans, discoverability and physical convenience matter.
At the same time, easier entry also means easier accidental entry, so I did not expose it everywhere.
I limited the shake-based entry point to debug and stage builds, not TestFlight or App Store builds.
That way internal users could reach the tool quickly when they actually needed it, without broadening the surface in production distributions.
The old version was technically possible. The new version was easier to actually use.
That difference matters more than we sometimes admit.
3. Removing the need to manually type non-default server addresses
There was another friction point in the same environment-switching flow.
When the selected country differed from the default landing country, people sometimes had to type the server environment address manually.
That meant the workflow depended on people knowing and entering the correct address themselves, which is exactly the kind of repeated manual step I want to eliminate.
I changed the setup so that this manual typing was no longer necessary.
The interesting part is that the solution was not adding more code.
It was simplifying the code path so the app no longer depended on that kind of manual address entry in the first place.
More specifically, I removed the manual server addresses and other hard-coded address values from the app-side flow and left only environment types such as debug, stage, and production.
That matters because the old setup could create a confusing mismatch.
For example, you could be using the app for country B, but the default address might still point to country A’s server. If the address was not typed correctly, you could suddenly end up looking at API responses for country A.
That is exactly the kind of failure mode I want to prevent: not just “the tool is inconvenient,” but “the tool makes the wrong state too easy to enter.”
With the simplified type-based flow, that kind of surprise became much harder to create in the first place.
I like this example because it is a good reminder that automation is not always about building another layer.
Sometimes the better automation is:
- remove hidden branching
- reduce special cases
- let the system carry the structure instead of the user
Less code can be the more automated solution.
4. Splitting regular release and hotfix versioning
Another source of avoidable friction was versioning.
One reason this stayed on my mind was a real bug discussion.
There was an Android-side issue, and someone from data analysis compared the user-behavior logs between iOS and Android. They correctly checked the iOS version that included the feature, but on Android they were looking at the wrong version.
Then an Android developer pointed out that the iOS and Android version gap at that moment was much larger than what had been referenced, so the Android version being discussed was probably not the right one.
That was a useful reminder for me: even version differences by themselves can create work.
They create extra interpretation steps before people can even start discussing the actual product behavior.
Originally, if the version was a.b.c, then both hotfixes and regular releases effectively pushed the patch digit forward. That meant a hotfix could distort the numbering flow of the next scheduled release.
Later, we also reached the point where the patch number had climbed high enough that we needed to roll the middle version forward anyway.
At that moment, someone reacted positively because it meant iOS and Android version numbers would match again for a while.
That was the moment the simpler rule clicked for me:
if people feel relief when the two platforms briefly happen to realign, then we should design the versioning rule so they stay aligned by default.
It also created communication overhead:
- people had to re-explain what kind of release a version actually was
- iOS and Android hotfix numbers could drift apart, which made the version story split across platforms
- release notes became more awkward to maintain
- a hotfix could force edits to release documentation that was already prepared
I proposed a clearer rule:
- regular releases increment
b - hotfixes increment
c
That proposal was accepted, and it reduced ambiguity immediately.
The benefit was not only cleaner numbers. It reduced coordination cost.
People could tell more quickly what a version meant, and hotfix deployment no longer created unnecessary follow-up edits in release documents that had already been written.
Sometimes the value of automation is not “do it with one click.” Sometimes it is “make the system communicate its own intent.”
What these changes had in common
These examples look different on the surface, but they share the same pattern.
Each one removed one of the following:
- a memory burden
- a typing burden
- an interpretation burden
- a documentation maintenance burden
None of them were flashy.
But that is exactly why I think they matter. A lot of useful engineering work is not about adding visible features. It is about giving time and attention back to the people building, testing, releasing, and supporting the product.
If I had to summarize the goal, it would be this:
Help people focus on the work that actually requires judgment, and stop making them spend that judgment on avoidable mechanics.
That is one of the most practical things programming can do.