A senior iOS developer's take on bloated codebases, legacy traps, and the 20 MB test.
I've been an iOS developer since 2008, focused on streaming and podcasting. I started working with HLS on iPhone in 2009, and over the years I've built, maintained, debugged, and sometimes inherited streaming apps of all sizes.
And I've developed a pretty strong opinion about what "clean code" actually means in this industry. Not the textbook version. The real one. The one you discover when you open someone else's Xcode project and your first instinct is to close the lid.
I've reviewed hundreds of job postings and freelance contracts for iOS developers over the years. Almost every single one proudly lists "clean code" and "best practices" as requirements. SOLID principles. MVVM. Unit test coverage. Design patterns. The whole playbook.
And then you open the project.
The gap between job postings and reality
Here's what I've actually found when opening those "clean code" streaming projects: XIB files from 2014 still driving critical UI flows. Objective-C bridging headers longer than some entire apps. Three different networking layers coexisting because nobody dared remove the old one. A Coordinator pattern stacked on top of a Navigator pattern stacked on top of a Router pattern, because each new lead architect added their favorite without removing the previous one.
"Clean code" in most streaming app job postings is aspirational, not descriptive. It describes what the hiring manager wishes the codebase looked like, not what awaits you in the repository.
Here's the reality check nobody writes in the job description: that Coordinator pattern they're so proud of? It coordinates three other navigation patterns nobody had time to remove. Those SOLID principles? They dissolved the moment the CEO wanted a feature shipped by Friday. The unit tests? They cover the login screen and absolutely nothing else.
I'm not throwing stones from a glass house. I've contributed to this kind of entropy myself. Every mobile developer has. But I think the streaming industry has a particularly bad version of this problem, and I have a theory about how to spot it instantly.
The 20 MB rule: a litmus test for streaming app code quality
Think about what a streaming application actually does. It fetches a manifest (an HLS playlist, a DASH MPD), feeds segments to a video player, renders some UI around it (channels, EPG, settings), and that's pretty much it. The content is streamed. The heavy stuff, the video, the audio, the thumbnails, all lives on a CDN, not in your app binary.
So here's my theory: if a streaming app weighs more than 20 MB on the App Store, every megabyte above that threshold is probably "dirty" code. Not malicious, not necessarily buggy, but code that shouldn't be there. Code that reflects poor architectural decisions accumulated over years of compromises.
Let's look at some numbers. My latest app, My TV Channel, weighs 9 MB on the App Store. Nine. It runs natively on iPhone, iPad, Mac (Apple Silicon), Apple TV, and Apple Vision Pro. It supports 9 languages. Users can create and watch linear TV channels 24/7, with VOD-to-Live scheduling, push notifications, offline downloads, picture-in-picture, multiview, advanced search with voice input, audio-only mode, private channels, and a full content management system for creators. That's more features than many mainstream streaming apps.
Go check the App Store yourself. Most major streaming apps weigh between 80 and 200 MB. Some go even higher. That's 10 to 20 times heavier than My TV Channel. These apps have the same primary job: stream video. Yet they carry the equivalent of dozens of My TV Channels in their binaries.
Sure, large streaming services need some of that extra weight. They support wide device matrices, accessibility features, offline DRM, sometimes games. But 150+ MB for what is, at its core, a video streaming application? That gap tells a story about legacy code, cross-platform compromises, and years of accumulated architectural debt.
Where the bloat comes from
After years of consulting on streaming projects, I keep seeing the same patterns inflating app binaries way beyond what's necessary.
Legacy UI technologies. XIB and Storyboard files are a classic one. They were the standard way to build iOS interfaces for years, and they embed serialized object graphs, layout constraints, sometimes even image references directly in the binary. Many streaming apps still carry XIBs from their initial release, patched and maintained but never migrated to SwiftUI or even modern UIKit patterns. Each XIB file is frozen technical debt with a .xib extension.
Cross-platform framework overhead. React Native, Flutter, Kotlin Multiplatform: each adds its own runtime, its own bridge layer, its own abstractions. For a streaming app where the most performance-critical component is the native video player anyway, the trade-off between cross-platform convenience and binary size rarely makes sense. You end up shipping a JavaScript engine (or a Dart VM, or a KMP runtime) just to render a grid of thumbnails that SwiftUI or UIKit handles in a fraction of the size.
It's the same logic that led backend teams down the microservices rabbit hole. Remember when every startup needed Kubernetes and 47 Docker containers to serve a REST API that a single Django app could handle? The mobile equivalent is shipping three framework runtimes to display a list of videos. The pattern is identical: adopting architectural complexity that solves problems you don't have, at a cost you'll pay for years.
Design pattern accumulation. This one is more subtle. It doesn't show up as megabytes directly, but it multiplies files, classes, and abstractions, which increases compile-time dependencies, embedded metadata, and binary size. I've seen streaming projects with a separate ViewModel, Coordinator, UseCase, Repository, DataSource, Mapper, and Entity for every single screen. That's seven files where two would do. Multiply by 30 screens and you have 210 files instead of 60, each one adding its class metadata to the binary.
VIPER is the worst offender here. It was designed for a UIKit world where massive view controllers were a real problem. Porting VIPER into a SwiftUI project is like bringing a fire truck to a candle. SwiftUI views are already lightweight, stateless, and composable by design. Wrapping each one in a VIPER stack (View, Interactor, Presenter, Entity, Router) adds five files and three layers of indirection for what SwiftUI handles with a struct and an @Observable class. I've seen teams do it anyway because "that's our architecture," and the result is always the same: more boilerplate than business logic.
Embedded assets that should be remote. Bundled fonts, bundled animations (Lottie JSON files are notorious for this), bundled placeholder images at 3x resolution for every device class. In a streaming app, almost every visual asset could be fetched on demand from a CDN. Bundling them is the lazy path, and it shows on the scale.
Dead code from abandoned features. A/B testing frameworks leave behind conditionals that never get cleaned up. Failed feature experiments stay in the codebase because "we might bring it back." The analytics SDK from two vendors ago still gets compiled because someone forgot to remove it from the Podfile.
The real cost: when dirty code decides your roadmap
Binary bloat is one thing. But the worst consequence of a dirty codebase isn't the download size. It's what happens in every sprint planning meeting.
You know the phrase. A product manager asks for a new feature, say picture-in-picture, audio-only mode, a simple UI refresh, and the first words out of the lead developer's mouth are: "It's gonna be complicated."
That sentence is the smell test for dirty code. Not "it will take time," not "we need to think about edge cases," but complicated. The codebase has become so tangled that nobody can predict what touching one module will break in three others.
In clean codebases, adding features feels natural. You find the right layer, you extend it, you ship. In dirty codebases, it feels like surgery on a patient with no medical records. Every change requires an archaeology expedition first. Every estimate comes with a risk multiplier nobody dares put on paper. Every sprint carries the unspoken caveat: "assuming nothing unexpected breaks."
I've seen streaming projects where implementing a simple "continue watching" feature required modifications across seven architectural layers, two bridging headers, and a custom event bus that nobody fully understood anymore. That's not software engineering. That's hostage negotiation with your own codebase.
And the part that product teams rarely grasp: dirty code doesn't just slow you down today. It decides what you can build tomorrow. Features don't get rejected because they're bad ideas. They get rejected because "our architecture doesn't support it," which is a polite way of saying "we built ourselves into a corner five years ago and nobody wants to admit it." The codebase becomes the de facto product manager, vetoing features through friction alone.
Reality check: your competitors with cleaner codebases will ship the same feature in two weeks while your team is still mapping dependency graphs in Miro. They're not smarter. They just don't have to fight their own code before fighting the market.
The LLM test: a new way to measure code cleanliness
Beyond binary size, there's another litmus test I've been using lately, one that didn't exist five years ago.
Point an LLM at your codebase. If it can't understand it, neither can your next hire.
Tools like Claude Code, OpenAI Codex, and other AI-assisted development environments are very good at navigating well-structured codebases. They can read a clean Swift project, understand its architecture, identify bugs, suggest fixes, and implement new features with minimal guidance.
But try pointing them at a legacy streaming app with 15 years of accumulated patterns, mixed Objective-C and Swift, three different dependency injection approaches, and a build system held together by custom shell scripts. The LLM will struggle. It will hallucinate relationships between classes that don't exist. It will suggest fixes that break other parts of the system. It will produce more crashes than a senior developer would.
That's not a limitation of AI. It's a mirror. If an LLM trained on millions of repositories can't parse your project's architecture, it means your abstractions are leaking, your naming is inconsistent, your dependencies are tangled, and your project structure doesn't follow any recognizable convention.
Think of it as the ultimate "new developer onboarding" test. An LLM approaches your codebase with zero institutional knowledge, just pattern recognition and broad understanding of programming conventions. If it gets lost, your next junior developer will get lost too. And your next senior developer will spend their first three months untangling the same knots, except they'll bill you for it.
Now flip this around. When your codebase is clean, something interesting happens: tools like Claude Code don't just review your code, they build with you. I've been using Claude Code on My TV Channel, and in a clean codebase, it can implement a new feature, write the tests, and open a PR in the time it used to take me to write a Jira ticket. It reads the architecture, understands the conventions, and produces code that fits.
This is where things get interesting for teams still running two-week sprints. Sprints were designed to manage human uncertainty: how long will this take, what can we commit to, when do we demo. But when an AI agent can reliably navigate your codebase and ship working code in hours, the sprint itself becomes the bottleneck. The planning meeting takes longer than the implementation. The ceremony around estimating a feature costs more time than building it.
Of course, this only works if the codebase is clean. Point Claude Code at a VIPER-infested, XIB-laden, multi-framework monster and it's back to hallucinating. The tool doesn't create the velocity. The clean architecture does. Claude Code just reveals it. And dirty codebases? They don't just slow down your human developers anymore. They also block AI from helping you, which is going to be an increasingly expensive handicap.
What clean code actually looks like in a streaming app
I built My TV Channel from scratch with these principles in mind. Here's what I think clean code means for streaming applications specifically.
Follow platform conventions. Apple's Human Interface Guidelines exist for a reason. Standard UIKit or SwiftUI components give you accessibility, Dynamic Type, Dark Mode, and localization for free. Every custom component you build instead is one you have to maintain, test across devices, and debug when Apple changes something in a new iOS release. My TV Channel runs on five Apple platforms with a shared codebase because it leans on platform-native UI, not because of some clever abstraction layer.
Keep the dependency graph shallow. A streaming app needs a video player, a networking layer, and a persistence layer. Beyond that, every third-party dependency should be questioned hard. Not all dependencies are bad. I use Kingfisher for image caching in My TV Channel because it does one thing well, it's actively maintained, and its binary footprint is reasonable for the value it provides. That's the bar. Does the dependency solve a real problem better than you could with platform APIs? Is it maintained? Is the size proportionate to the value? If the answer to any of those is no, it shouldn't be in your Podfile. The problem isn't using libraries. The problem is using five of them when one would do, or using a 5 MB reactive framework when Swift's native async/await and Combine handle the same job with zero external code.
Treat binary size as a feature, not a metric. Users on cellular networks, users with 64 GB iPhones, users in markets where storage is tight: they all benefit from a smaller app. Apple shows app size prominently on the App Store for a reason. A 9 MB download installs in seconds on any connection. A 150 MB download needs Wi-Fi for many users and competes for space with photos, messages, and other apps they care more about.
Delete code. The hardest part of maintaining a clean codebase isn't writing new code. It's deleting old code. Feature flags that haven't been toggled in six months, analytics events nobody checks in the dashboard, migration paths for data formats from three versions ago. All of it needs to go. The best code is the code that doesn't exist.
A challenge to the industry
Streaming is one of the most competitive categories on the App Store. Yet most OTT apps ship binaries 5x to 20x larger than they need to be, carry years of legacy code, and follow architectural patterns that even AI can't untangle.
Next time you see a job posting for an iOS developer at a streaming company that demands "clean code practices" and "modern architecture," ask them one question: how big is your app binary?
If the answer is north of 50 MB for what is fundamentally a video player with a content catalog, you know exactly what's waiting for you in that repository.
And if you want to see what a streaming app looks like when it's built from scratch with clean code principles, no legacy, no cross-platform compromises, no accumulated debt: download My TV Channel and check the size yourself.
9 MB. Five platforms. Full-featured. That's what clean code looks like.