De kijk van een iOS-ontwikkelaar op opgeblazen codebases, legacy-valkuilen en de 20 MB-test.
Ik ben iOS-ontwikkelaar sinds 2008, met een focus op streaming en podcasting. Ik begon in 2009 met HLS op de iPhone te werken, en in de loop der jaren heb ik streaming-apps van alle formaten gebouwd, onderhouden, gedebugd en soms geërfd.
En ik heb een behoorlijk uitgesproken mening ontwikkeld over wat "clean code" werkelijk betekent in deze branche. Niet de leerboekversie. De echte. Die je ontdekt wanneer je het Xcode-project van iemand anders opent en je eerste instinct is om het deksel dicht te klappen.
Ik heb door de jaren heen honderden vacatures en freelance-contracten voor iOS-ontwikkelaars bekeken. Bijna elke vacature vermeldt trots "clean code" en "best practices" als vereisten. SOLID-principes. MVVM. Unit test-dekking. Design patterns. Het volledige draaiboek.
En dan open je het project.
De kloof tussen vacatures en werkelijkheid
Dit is wat ik werkelijk heb aangetroffen bij het openen van die "clean code" streaming-projecten: XIB-bestanden uit 2014 die nog steeds kritieke UI-flows aansturen. Objective-C bridging headers die langer zijn dan sommige complete apps. Drie verschillende netwerklagen die naast elkaar bestaan omdat niemand het aandurfde de oude te verwijderen. Een Coordinator-pattern gestapeld op een Navigator-pattern gestapeld op een Router-pattern, omdat elke nieuwe lead architect zijn favoriete patroon toevoegde zonder het vorige te verwijderen.
"Clean code" in de meeste vacatures voor streaming-apps is een wensdroom, geen beschrijving van de werkelijkheid. Het beschrijft wat de hiring manager zou willen dat de codebase eruitzag, niet wat je te wachten staat in de repository.
Hier is de realiteitscheck die niemand in de vacaturetekst zet: dat Coordinator-pattern waar ze zo trots op zijn? Het coördineert drie andere navigatiepatronen die niemand tijd had om te verwijderen. Die SOLID-principes? Die losten op zodra de CEO een feature voor vrijdag wilde. De unit tests? Die dekken het inlogscherm en verder absoluut niets.
Ik gooi geen stenen vanuit een glazen huis. Ik heb zelf bijgedragen aan dit soort entropie. Elke mobiele ontwikkelaar heeft dat. Maar ik denk dat de streaming-branche een bijzonder erge versie van dit probleem heeft, en ik heb een theorie over hoe je het direct kunt herkennen.
De 20 MB-regel: een lakmoesproef voor codekwaliteit van streaming-apps
Denk eens na over wat een streaming-applicatie eigenlijk doet. Het haalt een manifest op (een HLS-playlist, een DASH MPD), voert segmenten naar een videospeler, rendert wat UI eromheen (kanalen, EPG, instellingen), en dat is het zo'n beetje. De content wordt gestreamd. De zware dingen – de video, de audio, de thumbnails – wonen allemaal op een CDN, niet in je app-binary.
Dus hier is mijn theorie: als een streaming-app meer dan 20 MB weegt in de App Store, is elke megabyte boven die drempel waarschijnlijk "vuile" code. Niet kwaadaardig, niet per se buggy, maar code die er niet hoort te zijn. Code die slechte architectuurbeslissingen weerspiegelt, opgestapeld over jaren van compromissen.
Laten we naar wat cijfers kijken. Mijn nieuwste app, My TV Channel, weegt 9 MB in de App Store. Negen. Het draait native op iPhone, iPad, Mac (Apple Silicon), Apple TV en Apple Vision Pro. Het ondersteunt 9 talen. Gebruikers kunnen lineaire TV-kanalen 24/7 aanmaken en bekijken, met VOD-to-Live-planning, pushmeldingen, offline downloads, beeld-in-beeld, multiview, geavanceerd zoeken met spraakinvoer, alleen-audio-modus, privékanalen en een volledig contentmanagementsysteem voor creators. Dat zijn meer functies dan veel mainstream streaming-apps.
Kijk zelf in de App Store. De meeste grote streaming-apps wegen tussen de 80 en 200 MB. Sommige gaan nog hoger. Dat is 10 tot 20 keer zwaarder dan My TV Channel. Deze apps hebben dezelfde primaire taak: video streamen. Toch dragen ze het equivalent van tientallen My TV Channels in hun binaries.
Natuurlijk hebben grote streamingdiensten iets van dat extra gewicht nodig. Ze ondersteunen brede apparaatmatrices, toegankelijkheidsfuncties, offline DRM, soms games. Maar 150+ MB voor wat in de kern een video-streaming-applicatie is? Dat verschil vertelt een verhaal over legacy-code, cross-platform-compromissen en jaren van opgestapelde architectuurschuld.
Waar de opgeblazenheid vandaan komt
Na jaren van consultancy bij streaming-projecten zie ik steeds dezelfde patronen die app-binaries ver boven het noodzakelijke opblazen.
Verouderde UI-technologieën. XIB- en Storyboard-bestanden zijn een klassieker. Ze waren jarenlang de standaardmanier om iOS-interfaces te bouwen, en ze embedden geserialiseerde objectgrafen, layout-constraints, soms zelfs afbeeldingsreferenties direct in de binary. Veel streaming-apps dragen nog steeds XIB's uit hun oorspronkelijke release mee, gepatcht en onderhouden maar nooit gemigreerd naar SwiftUI of zelfs moderne UIKit-patronen. Elk XIB-bestand is bevroren technische schuld met een .xib-extensie.
Overhead van cross-platform frameworks. React Native, Flutter, Kotlin Multiplatform: elk voegt zijn eigen runtime toe, zijn eigen bridge-laag, zijn eigen abstracties. Voor een streaming-app waar het meest prestatiekritische onderdeel sowieso de native videospeler is, weegt de afweging tussen cross-platform-gemak en binary-grootte zelden op. Je eindigt met het leveren van een JavaScript-engine (of een Dart VM, of een KMP-runtime) alleen maar om een raster van thumbnails te renderen dat SwiftUI of UIKit in een fractie van de grootte afhandelt.
Het is dezelfde logica die backend-teams in het microservices-konijnenhol leidde. Weet je nog toen elke startup Kubernetes en 47 Docker-containers nodig had om een REST API te serveren die een enkele Django-app aankon? Het mobiele equivalent is drie framework-runtimes leveren om een lijst met video's te tonen. Het patroon is identiek: architecturale complexiteit omarmen die problemen oplost die je niet hebt, tegen een prijs die je jarenlang betaalt.
Ophoping van design patterns. Dit is subtieler. Het manifesteert zich niet direct als megabytes, maar het vermenigvuldigt bestanden, klassen en abstracties, wat compile-time-afhankelijkheden, ingebedde metadata en binary-grootte vergroot. Ik heb streaming-projecten gezien met een apart ViewModel, Coordinator, UseCase, Repository, DataSource, Mapper en Entity voor elk afzonderlijk scherm. Dat zijn zeven bestanden waar twee zouden volstaan. Vermenigvuldig met 30 schermen en je hebt 210 bestanden in plaats van 60, waarbij elk bestand zijn klasse-metadata aan de binary toevoegt.
VIPER is hier de grootste boosdoener. Het werd ontworpen voor een UIKit-wereld waar massieve view controllers een echt probleem waren. VIPER overzetten naar een SwiftUI-project is als een brandweerwagen meenemen naar een kaars. SwiftUI views zijn al van nature lichtgewicht, stateless en composable. Elk ervan inpakken in een VIPER-stack (View, Interactor, Presenter, Entity, Router) voegt vijf bestanden en drie lagen van indirectie toe voor iets dat SwiftUI afhandelt met een struct en een @Observable klasse. Ik heb teams het toch zien doen omdat "dat is onze architectuur," en het resultaat is altijd hetzelfde: meer boilerplate dan bedrijfslogica.
Ingebedde assets die remote zouden moeten zijn. Gebundelde lettertypen, gebundelde animaties (Lottie JSON-bestanden zijn hier berucht om), gebundelde placeholder-afbeeldingen in 3x-resolutie voor elke apparaatklasse. In een streaming-app zou vrijwel elke visuele asset on demand van een CDN opgehaald kunnen worden. Ze bundelen is de luie weg, en dat is te zien aan de omvang.
Dode code van verlaten functies. A/B-testing frameworks laten condities achter die nooit worden opgeruimd. Mislukte feature-experimenten blijven in de codebase omdat "we brengen het misschien terug." De analytics-SDK van twee leveranciers geleden wordt nog steeds gecompileerd omdat iemand vergat het uit de Podfile te verwijderen.
De werkelijke kosten: wanneer vuile code je roadmap bepaalt
Binary-opgeblazenheid is één ding. Maar de ergste consequentie van een vuile codebase is niet de downloadgrootte. Het is wat er in elk sprint-planningsvergadering gebeurt.
Je kent de zin. Een product manager vraagt om een nieuwe functie – zeg beeld-in-beeld, alleen-audio-modus, een simpele UI-vernieuwing – en de eerste woorden van de lead developer zijn: "Het wordt ingewikkeld."
Die zin is de geurtest voor vuile code. Niet "het kost tijd," niet "we moeten over edge cases nadenken," maar ingewikkeld. De codebase is zo verstrikt geraakt dat niemand kan voorspellen wat het aanraken van één module kapot zal maken in drie andere.
In schone codebases voelt het toevoegen van functies natuurlijk aan. Je vindt de juiste laag, breidt het uit, levert het op. In vuile codebases voelt het als een operatie op een patiënt zonder medisch dossier. Elke wijziging vereist eerst een archeologische expeditie. Elke schatting komt met een risicovermenigvuldiger die niemand op papier durft te zetten. Elke sprint draagt het onuitgesproken voorbehoud: "ervan uitgaande dat er niets onverwachts kapotgaat."
Ik heb streaming-projecten gezien waar het implementeren van een simpele "verder kijken"-functie wijzigingen vereiste in zeven architectuurlagen, twee bridging headers en een custom event bus die niemand meer volledig begreep. Dat is geen software engineering. Dat is gijzelingsonderhandeling met je eigen codebase.
En het deel dat productteams zelden begrijpen: vuile code vertraagt je niet alleen vandaag. Het bepaalt wat je morgen kunt bouwen. Functies worden niet afgewezen omdat het slechte ideeën zijn. Ze worden afgewezen omdat "onze architectuur het niet ondersteunt," wat een beleefde manier is om te zeggen "we hebben onszelf vijf jaar geleden in een hoek gebouwd en niemand wil het toegeven." De codebase wordt de de facto product manager, die functies vetoëert door pure frictie.
Realiteitscheck: je concurrenten met schonere codebases leveren dezelfde functie in twee weken terwijl jouw team nog afhankelijkheidsgrafen in kaart brengt in Miro. Ze zijn niet slimmer. Ze hoeven alleen niet tegen hun eigen code te vechten voordat ze tegen de markt vechten.
De LLM-test: een nieuwe manier om codeschoonheid te meten
Naast binary-grootte is er nog een lakmoesproef die ik de laatste tijd gebruik – eentje die vijf jaar geleden niet bestond.
Richt een LLM op je codebase. Als het die niet kan begrijpen, kan je volgende aanwinst dat ook niet.
Tools zoals Claude Code, OpenAI Codex en andere AI-ondersteunde ontwikkelomgevingen zijn erg goed in het navigeren door goed gestructureerde codebases. Ze kunnen een schoon Swift-project lezen, de architectuur begrijpen, bugs identificeren, fixes voorstellen en nieuwe functies implementeren met minimale begeleiding.
Maar probeer ze eens te richten op een legacy streaming-app met 15 jaar aan opgestapelde patronen, gemengd Objective-C en Swift, drie verschillende dependency injection-benaderingen en een build-systeem dat bij elkaar wordt gehouden door custom shell-scripts. Het LLM zal worstelen. Het zal relaties hallucineren tussen klassen die niet bestaan. Het zal fixes voorstellen die andere delen van het systeem kapotmaken. Het zal meer crashes produceren dan een senior ontwikkelaar zou doen.
Dat is geen beperking van AI. Het is een spiegel. Als een LLM dat getraind is op miljoenen repositories de architectuur van jouw project niet kan ontleden, betekent het dat je abstracties lekken, je naamgeving inconsistent is, je afhankelijkheden verstrikt zijn en je projectstructuur geen herkenbare conventie volgt.
Zie het als de ultieme "onboarding-test voor nieuwe ontwikkelaars." Een LLM benadert je codebase met nul institutionele kennis, alleen patroonherkenning en brede kennis van programmeerconventies. Als het verdwaalt, verdwaalt je volgende junior ontwikkelaar ook. En je volgende senior ontwikkelaar zal de eerste drie maanden besteden aan het ontwarren van dezelfde knopen – alleen stuurt die er een factuur voor.
Draai het nu om. Wanneer je codebase wél schoon is, gebeurt er iets interessants: tools zoals Claude Code reviewen niet alleen je code, ze bouwen mee. Ik gebruik Claude Code bij My TV Channel, en in een schone codebase kan het een nieuwe functie implementeren, de tests schrijven en een PR openen in de tijd die het me vroeger kostte om een Jira-ticket te schrijven. Het leest de architectuur, begrijpt de conventies en produceert code die past.
Dit is waar het interessant wordt voor teams die nog steeds tweewekelijkse sprints draaien. Sprints werden ontworpen om menselijke onzekerheid te beheren: hoe lang duurt dit, waar kunnen we ons aan committen, wanneer demo'en we. Maar wanneer een AI-agent betrouwbaar door je codebase kan navigeren en werkende code kan leveren in uren, wordt de sprint zelf de bottleneck. De planningsvergadering duurt langer dan de implementatie. De ceremonie rondom het schatten van een functie kost meer tijd dan het bouwen ervan.
Dit werkt natuurlijk alleen als de codebase schoon is. Richt Claude Code op een VIPER-geïnfecteerd, XIB-beladen, multi-framework-monster en het is terug naar hallucineren. De tool creëert niet de snelheid. De schone architectuur doet dat. Claude Code maakt het alleen zichtbaar. En vuile codebases? Die vertragen niet alleen meer je menselijke ontwikkelaars. Ze blokkeren ook AI om je te helpen – en dat wordt een steeds duurdere handicap.
Hoe clean code er werkelijk uitziet in een streaming-app
Ik heb My TV Channel vanaf nul gebouwd met deze principes in gedachten. Dit is wat ik denk dat clean code specifiek betekent voor streaming-applicaties.
Volg platformconventies. Apples Human Interface Guidelines bestaan niet voor niets. Standaard UIKit- of SwiftUI-componenten geven je gratis toegankelijkheid, Dynamic Type, Dark Mode en lokalisatie. Elk custom component dat je in plaats daarvan bouwt, is er een dat je moet onderhouden, testen op verschillende apparaten en debuggen wanneer Apple iets verandert in een nieuwe iOS-release. My TV Channel draait op vijf Apple-platforms met een gedeelde codebase omdat het leunt op platformnative UI – niet vanwege een of andere slimme abstractielaag.
Houd de afhankelijkheidsgrafiek ondiep. Een streaming-app heeft een videospeler nodig, een netwerklaag en een persistentielaag. Daarbuiten moet elke third-party-afhankelijkheid kritisch worden bevraagd. Niet alle afhankelijkheden zijn slecht. Ik gebruik Kingfisher voor image caching in My TV Channel omdat het één ding goed doet, het wordt actief onderhouden, en de binary-footprint is redelijk voor de waarde die het biedt. Dat is de lat. Lost de afhankelijkheid een echt probleem beter op dan je zou kunnen met platform-API's? Wordt het onderhouden? Is de grootte proportioneel aan de waarde? Als het antwoord op een van deze vragen nee is, hoort het niet in je Podfile. Het probleem is niet het gebruiken van bibliotheken. Het probleem is er vijf gebruiken wanneer één zou volstaan, of een 5 MB reactief framework gebruiken terwijl Swift's native async/await en Combine dezelfde taak afhandelen zonder externe code.
Behandel binary-grootte als een functie, niet als een metriek. Gebruikers op mobiele netwerken, gebruikers met 64 GB iPhones, gebruikers in markten waar opslagruimte schaars is: ze profiteren allemaal van een kleinere app. Apple toont de app-grootte prominent in de App Store, en dat is niet zonder reden. Een download van 9 MB installeert in seconden op elke verbinding. Een download van 150 MB vereist Wi-Fi voor veel gebruikers en concurreert om ruimte met foto's, berichten en andere apps waar ze meer om geven.
Verwijder code. Het moeilijkste deel van het onderhouden van een schone codebase is niet het schrijven van nieuwe code. Het is het verwijderen van oude code. Feature flags die al zes maanden niet zijn omgezet, analytics-events die niemand in het dashboard controleert, migratiepaden voor dataformaten van drie versies geleden. Het moet allemaal weg. De beste code is de code die niet bestaat.
Een uitdaging aan de branche
Streaming is een van de meest competitieve categorieën in de App Store. Toch leveren de meeste OTT-apps binaries die 5 tot 20 keer groter zijn dan nodig, dragen ze jarenlange legacy-code mee en volgen ze architectuurpatronen die zelfs AI niet kan ontwarren.
De volgende keer dat je een vacature ziet voor een iOS-ontwikkelaar bij een streaming-bedrijf dat "clean code-praktijken" en "moderne architectuur" eist, stel dan één vraag: hoe groot is jullie app-binary?
Als het antwoord boven de 50 MB ligt voor wat fundamenteel een videospeler met een contentcatalogus is, weet je precies wat je te wachten staat in die repository.
En als je wilt zien hoe een streaming-app eruitziet wanneer die vanaf nul is gebouwd met clean code-principes – geen legacy, geen cross-platform-compromissen, geen opgestapelde schuld: download My TV Channel en bekijk de grootte zelf.
9 MB. Vijf platforms. Volledig uitgerust. Zo ziet clean code eruit.