I rebuilt iMessage line for line. Nobody wanted it.
I spent a year rebuilding iMessage in SwiftUI. Not a chat UI “inspired by” iMessage. iMessage. The bubble tails that only draw on the last message in a run. The typing dots that breathe in a wave instead of blinking. The keyboard you can drag down with your finger while the messages stay welded to it. “Delivered” fading into “Read” at exactly the right moment. I wrote up the whole build separately, and I stand behind every line of it. It might be the best screen I’ve ever made.1
Then I packaged it up, shipped it, and nobody wanted it.
This is the thing nobody wanted, the real package running on my phone, filmed this week:
The effort side of the ledger
If you read the build post, you know the effort was real. What I hadn’t done until writing this is count it.
| Commits | 590 |
| Days with a commit | 77 |
| First commit | August 11, 2025 |
| Most recent commit | August 29, 2026 |
| Lines written | 29,300 |
| Lines deleted | 13,341 |
| Lines still standing | 14,975 |
ChatViewController.swift | 3,573 lines |
| Commits mentioning the bubble | 88 |
| Commits mentioning the tail | 27 |
- Nov 8, 2025
- Nov 8, 2025
- Nov 10, 2025
- Nov 10, 2025
- Nov 10, 2025
- Nov 10, 2025
- Nov 10, 2025
- Nov 10, 2025
- Nov 11, 2025
- Nov 11, 2025
- Nov 11, 2025
- Nov 11, 2025
- Nov 14, 2025
- Nov 18, 2025
- Dec 3, 2025
- Dec 3, 2025
- Feb 3, 2026
- Jul 29, 2026
- Aug 2, 2026
- Aug 2, 2026
- Aug 2, 2026
- Aug 9, 2026
- Aug 9, 2026
- Aug 9, 2026
- Aug 10, 2026
- Aug 10, 2026
- Aug 26, 2026
main whose message mentions the tail, oldest first. The first one is nine days before launch. The last one is this week.The scrolling isn’t SwiftUI at all; it’s a UICollectionView wearing a SwiftUI costume, because List couldn’t do what iMessage does, and that one file is a quarter of the package. The keyboard drag is corrected frame by frame. The bubbles sit on actual springs. The bubble shape is hand-traced. The haptic on receive is a custom pattern I matched to Messages by feel. Here is what each of those sentences costs, filmed on my phone. Try them at 0.5x.




Eleven people have commits in that repo. I wrote about three in five of them; the rest came from Rafi, Aaron, John, Dimitri, Glenn, Kiran, Yohan, Gokul, Parth and Vikrant. The tail shape that finally worked came in as someone else’s pull request.
None of that was necessary. All of it was the point. I wanted the thing you could drop into any app and have it feel like Apple built it. And I got there.
Ship day
I polished the README. I made the screenshots, in both themes. I built an entire separate demo app so people could try it in one clone.2

Then I pushed the package public, wrote it up here, told some people, and I think tweeted about it once or twice. I didn’t do more than that. I was scared to, and I can say that now.
Nothing happened.
I want to be precise here, because founders love to fuzz this part. This is not a story about a launch that underperformed, because there was barely a launch. This is a story about silence. Here is that repository today, nine months later:
| Stars | 8 |
| Forks | 0 |
| Watchers | 0 |
| Issues ever opened | 0 |
| Views, last 14 days | 21 (8 unique) |
| Clones, last 14 days | 8 (6 unique) |
| Top referrer | github.com |
Eight clones in two weeks, and I can account for most of them. The kind of silence where you start checking whether the link works.3
The comparison that stung
The tempting excuse is that Swift packages just don’t get traction. Except I have a shelf of them, published by the same person, from the same org, promoted the same way, and I can put them side by side.
union-tab-view is a tab bar. It went up eleven days after union-chat, it is a fourteenth of the code, and it has fifteen times the stars. union-confetti is 435 lines that throw confetti at the screen, and it beat union-chat three to one.
The market wasn’t rejecting Swift packages. It was rejecting this one.
The wrong diagnosis
My first theory was distribution. Wrong day to post. Wrong channels. Needed a demo video. Needed a better hook. I know how to get attention when I want it. The last thing I gave away like this became the most upvoted post in the history of r/swift, so this was the comfortable explanation: the product was great, the launch was timid, fixable. And the theory had the advantage of being partly true. I had barely told anyone.
It took me embarrassingly long to accept the real explanation, which is that a real launch would have changed the number and not the answer: you can’t market your way out of a product nobody was waiting for, and nobody was waiting for this.
Here’s the cold math I skipped before building. How many people are making an iOS app with a chat screen this year? Some. How many of those want their chat to look exactly like iMessage, instead of like their own brand? Fewer. How many of those would rather adopt a dependency than build it themselves and control it? Fewer still. And how many of that sliver would ever find my package?
I never ran that math because I didn’t want the answer. Nobody sits at their desk furious that their typing indicator doesn’t breathe. I was chasing a standard, and the standard was mine.
The reactions I did get were all some version of “wow, impressive.” Not one was “finally.” Impressive is what people say when they’re not going to use it.
That’s the distinction I couldn’t see from inside: impressive is not the same as wanted. Impressive gets you compliments. Wanted gets you users.
The diagnosis I skipped
That’s the honest lesson and I believe it. But writing this post made me go look at the artifact instead of the feeling, and the artifact has a second explanation on it in plain sight.
The thing I said you could drop into any app ships as a closed-source binary. The public repo is 35 lines of Swift: a thin wrapper around an xcframework downloaded from a release. You cannot read it, patch it, or audit it. It requires iOS 18, which in November 2025 meant walking away from a large share of everyone’s install base. It pulls in a third-party image library as a hard dependency. And in forty-six releases it has never once left beta. The newest tag is 0.7.1-beta.23.
This is the entire public Package.swift, which is what an adopter reads before deciding:
let package = Package(
name: "union-chat",
platforms: [
.iOS(.v18)
],
products: [
.library(
name: "UnionChat",
targets: ["UnionChatWrapper", "UnionChatBinary"])
],
dependencies: [
.package(url: "https://github.com/onevcat/Kingfisher.git", from: "8.0.0")
],
targets: [
.target(
name: "UnionChatWrapper",
dependencies: [
"UnionChatBinary",
.product(name: "Kingfisher", package: "Kingfisher")
],
path: "Sources/UnionChatWrapper"
),
.binaryTarget(
name: "UnionChatBinary",
url: "https://github.com/unionst/union-chat/releases/download/0.7.1-beta.23/UnionChat.xcframework.zip",
checksum: "da7d56072f3834b4a5c67bd064ec264cbb8a575b60c7a8b9b2256291b41bf67e"
)
]
)Put yourself in the shoes of the one developer who did find it. They wanted a chat screen. They found an unreadable beta blob, from an author they’ve never heard of, that raises their deployment target and adds a dependency, with a version number that says don’t.
I have been telling this story as “the market didn’t want the perfect thing.” Some of it is “I shipped the perfect thing in a package nobody sane would take a risk on.” Both are true. The second one is more useful, and I couldn’t see it while I was busy being wounded about the first.
It never actually stopped
Here’s the part I didn’t know about myself until I plotted it.
Two hundred and eighty-one commits in the month I launched. Then a decay, and then March through June where I touched it zero times, four months running. The sulk, rendered. And then it comes back, hard, in the summer.
Not because someone starred it.
The coda
Five weeks after launch, union-chat went into Wavelength, the music app I actually run, to draw the comments under a review. I didn’t count that as a user because the user was me. By this summer it was the whole messaging surface. Eleven files import it. It is the chat of a real product with real users, and every detail I ground on for a year, the collated tails, the delivery label, the keyboard welded to the input bar, is load-bearing in an app people open.
Wavelength, this month. That’s union-chat, pink: tail on the last bubble of each run, sender names above each run, “Delivered” under the outgoing message.
And the joke, which I love, is on me. Wavelength doesn’t depend on the package I published. Its Package.resolved points at union-chat-source, the private repository, pinned to a commit on main. And Wavelength isn’t alone. Polymarket runs a version of it too: they forked the source and rewrote a great deal of it. Two apps in the world run this code, and neither one uses the package. Not even I would adopt the thing I shipped.
This month is the busiest that repo has had since launch, 155 commits, and every one of them exists because a real app needed something. Demand arrived from inside the building, and the work instantly got better for having a user.
I don’t regret it. The work made me better in ways client projects never could have, because no client will ever pay for the thirty-first commit about a bubble tail.4 Every trick in that package went straight into everything I’ve shipped since, and now into a product I own.
And no, I’m not opening the source. There is too much in it now. So the package stays exactly what it is: free, closed, and unadopted, while the code inside it runs in two real apps.
Nobody wanted it. Then, eventually, one person did, and it turned out to be me. That’s a worse business plan than I’d like and a better ending than I expected.
Footnotes
-
I’m biased, but I’m also documented. The build post has the receipts: the springs, the frame-by-frame keyboard math, the hand-traced bubble. ↩
-
The demo app also has no users. It’s a matched set. ↩
-
It worked. ↩
-
In the draft of this post I wrote “the ninth revision of a bubble tail” and called it a rhetorical number. The real one, counted out of the commit log: twenty-seven commits with “tail” in the message, the first nine days before going public and the most recent this week. Nine months, on the corner of a bubble. ↩