← Writing

The rewrite that was never a project

I went looking through three years of commits recently and found something that surprised me. The chat system in our iOS app moved from UIKit to SwiftUI. I remember doing it. It is not in the history — or rather, it is in the history as exactly one commit, in November 2023, sitting between a fix to message cell padding and some work on chat muting.

There are 383 commits touching chat across those three years. One of them mentions SwiftUI. The framework migration that I would describe in conversation as a piece of work was, in the record, a Tuesday.

What chat actually turned into

Worth establishing the scale first, because the point does not land otherwise.

The app did not set out to contain a chat product. It was a softphone: make calls, see colleagues, look things up. Chat began as messaging attached to contacts, and then over three years accumulated threaded replies, quoted messages, reactions, pinning, read and unread state, mark-as-unread that clears itself when a new message arrives, muting with global settings, system messages, embedded video, image and video capture, file attachments, URL parsing, user tagging, markdown rendering, group creation, archiving, search, and localised date headers.

That is a chat client. Nobody ever wrote down that we were building one. Each piece arrived as a reasonable request — the desktop client has reactions, customers expect threads, somebody needs to mute a noisy group — and each was a couple of weeks at most.

Why the migration has no ceremony

The framework change happened the same way, and looking back I think that was correct rather than sloppy.

The honest version is that SwiftUI arrived where it was easier, which was the newer and more self-contained views, and did not arrive where it was not. The commit that names it describes switching the chat view to act as a SwiftUI view with the UIKit side as delegate — which is the hinge, not the whole thing. Around it, incrementally, new screens were written in the new framework and old ones were left alone until something needed changing anyway.

Nobody proposed a migration. Nobody estimated one. There was no ticket called "SwiftUI migration" because there was never a moment where stopping feature work to change frameworks would have been the obvious call — and if I had proposed it, the honest business answer would have been no.

The advantage of not having a project

Framework migrations proposed as projects have a characteristic failure. They are large, they deliver nothing visible, they are scoped against a codebase that keeps changing underneath them, and they get paused halfway when something urgent happens — leaving you with two frameworks and no benefit, which is worse than either.

Doing it opportunistically avoids most of that. Every screen you touch anyway is a chance to move it. You are never more than one commit from a shippable state. If the effort stops, you keep whatever you converted and nothing is half-done. And the decision is made per screen with real information — is this view actually easier in the new framework? — rather than once, in advance, as an article of faith.

The interop is what makes it possible, and it is also the thing that gets undersold. Being able to host one framework inside the other, in both directions, is what turns a migration from a cliff into a ramp. A lot of the real work is in the seams: making a view behave correctly when its parent belongs to the other world, and keeping state ownership clear across the boundary so that two frameworks are not both convinced they are managing the same thing.

The cost of not having a project

I do not want to be glib about it, because there are real downsides and I lived with them.

You are in both frameworks for years. Not months. Every developer needs to be fluent in two ways of building a screen, and every new piece of work starts with a small decision about which world it belongs in. That tax is permanent until the last old screen is gone, and the last old screens are the ones nobody touches, which is why they are still old.

It is invisible. There is no artefact. No design doc, no before-and-after, nothing to point at in a review. I only reconstructed the shape of it by searching the commit history for this piece, and if you had asked me last month when it happened I would have guessed wrong by about six months.

Inconsistency is the default. Two frameworks means two sets of conventions for spacing, navigation, state, and animation, and they diverge silently unless someone is deliberately holding them together. A meaningful share of the chat commits are exactly that — padding brought back in line, backgrounds matched to the desktop client, an indicator that did not update because the view did not know it should.

What I would tell myself

Incremental migration is a legitimate strategy, not a failure to plan. For a framework change in a product under active development, it is usually the only one that finishes. The version that gets proposed, scoped and paused is the one that leaves you worst off.

But write down that you are doing it. This is the bit I would change. Not a project — a paragraph. What we are moving to, what triggers a conversion, what we are deliberately not converting. It costs nothing and it turns an accumulation of individual judgements into a thing the team is doing on purpose. It would also have meant the next person did not have to reverse-engineer the intent from a commit about view delegates.

Watch what accretes. The more interesting finding was not the framework change. It was that a softphone quietly grew a full chat client, one reasonable request at a time, and nobody ever stood back and said that out loud. Every one of those decisions was right on its own and the sum of them was a second product inside the first, maintained as though it were a feature.

Also in this series