Skip to content
By • Published • Updated

White-Label iOS Architecture: One Swift Codebase, Sixteen Storefronts

Most articles about white-label mobile apps stop at “use build configurations for your colors.” That advice survives about three brands. The problems that decide whether a white-label program is maintainable show up somewhere around the eighth: release trains that no longer fit in a sprint, a test matrix nobody can run, and feature code slowly filling with the name of one particular brand.

This is a structural guide to shipping one Swift codebase as many branded apps: what belongs in configuration, what belongs in code, how the Xcode project is wired, and which decisions are expensive to reverse once you have store listings in production.

It assumes native iOS. If you are weighing a cross-platform stack for a multi-brand programme instead, Flutter vs React Native covers that decision, and the SwiftUI migration playbook covers the boundary problem that shows up when brands sit on different UI stacks.

The Decision That Sets Everything Else

Before any architecture work, answer one question: does each brand need its own App Store listing?

If yes, each brand needs its own bundle identifier, provisioning profile, App Store Connect record, and privacy manifest. That is a build-time concern, and it forces a configuration-per-brand structure. If no (a single app that re-skins itself after the user picks a tenant) the entire problem becomes runtime configuration, and most of this article does not apply to you.

The two models look similar in a diagram and behave nothing alike in practice. Mixing them is the most expensive mistake available here, because the fix is a migration of every brand at once.

A second question follows: whose developer account publishes each app? Your own team is simpler to automate: one certificate, one App Store Connect API key. Each brand’s own account is what retail clients usually want, because the listing names them as the seller, and it means a DEVELOPMENT_TEAM per brand and sixteen sets of credentials for CI to hold. Decide before the first listing exists; transferring apps between accounts later is manual, restricted, and sixteen times over.

Targets, Schemes, and What Scales

For separate store listings, the instinct is one Xcode target per brand. Resist it. Targets duplicate build settings and build phases, so sixteen targets means sixteen places to forget a setting and sixteen places to add every new script phase. Product variation belongs somewhere cheaper to edit.

The structure that holds up:

  • One application target containing nothing brand-specific.
  • One pair of build configurations per brand (for example Debug-Acme and Release-Acme), each backed by that brand’s .xcconfig file, which in turn includes a shared base file.
  • One scheme per brand that selects those configurations for Run, Test, Profile, and Archive.
  • Feature code in local Swift packages that cannot import brand configuration at all, so the compiler enforces the boundary you are trying to maintain.

A brand file ends up looking like this:

// Config/Brands/Acme.xcconfig
#include "../Base.xcconfig"

BRAND_KEY = acme
PRODUCT_BUNDLE_IDENTIFIER = com.acme.store
INFOPLIST_KEY_CFBundleDisplayName = Acme
DEVELOPMENT_TEAM = ABCDE12345
CODE_SIGN_ENTITLEMENTS = Config/Brands/Acme.entitlements
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon-Acme
API_BASE_URL = api.acme.example

// Only this brand's resources are compiled into the bundle
EXCLUDED_SOURCE_FILE_NAMES = Brands/*
INCLUDED_SOURCE_FILE_NAMES = Brands/Acme/*

Two of those lines carry most of the weight. EXCLUDED_SOURCE_FILE_NAMES and INCLUDED_SOURCE_FILE_NAMES let every brand’s asset catalog and theme file live in the one target while only the selected brand’s copies are compiled in: no runtime lookup, no naming collisions. And BRAND_KEY reaches the app through one Info.plist entry ($(BRAND_KEY)) read once at launch by exactly one type.

The bullet about packages is the one that matters in year two. Rules stated in a wiki decay. Rules expressed as module boundaries do not, because violating them stops the build.

Classify What Varies Before You Build Anything

Every white-label program has four categories of variation, and they want different mechanisms. Conflating them is what produces unmaintainable code.

  • Identity: name, icon, colors, typography, imagery, tone. Pure data. Belongs in asset catalogs and a theme definition, never in code.
  • Environment: API hosts, tenant identifiers, third-party SDK keys, analytics destinations. Build configuration, injected once at launch.
  • Capability: which features this brand has bought or enabled. A flag resolved behind an interface, never a brand check.
  • Regulatory: consent copy, data residency, retention, accessibility commitments. This one is genuinely hard, because it can change control flow rather than presentation, and it is the category most likely to tempt someone into a brand conditional.

The rule that keeps the codebase alive: feature code never names a brand. No if brand == .someRetailer, ever. A feature asks whether a capability is enabled or reads a themed value. If a feature needs to know which brand it is running as, the variation was misclassified, usually a capability that was never modelled. A brand that must show a consent screen before analytics start is not “brand X”; it is a brand with requiresAnalyticsConsent enabled, and the next brand with the same obligation costs a line in a manifest.

The Theme Layer

Theming fails in a predictable way: it starts as a color palette, then someone needs a different corner radius, then a different button height, then a brand needs an entirely different navigation pattern, and now the theme is a god object.

Two constraints prevent that:

  • A theme carries values, not decisions. Colors, spacing, typography, corner radii, asset names. The moment a theme contains a boolean that changes behavior, that value was a capability flag wearing a costume.
  • Semantic naming, not descriptive naming. surfacePrimary, not lightGray. Descriptive names stop being true the first time a brand ships a dark identity, and the rename touches every file.

Asset catalogs do more here than people expect. Because only the selected brand’s catalog is compiled in, every brand uses the same asset names (brand/logo, brand/hero), and a missing asset fails a conformance test instead of shipping as a blank image. Catalog colors also give you light and dark variants per brand for free. Build the theme once at launch, inject it at the root, and let nothing else construct one.

Release Management Is the Real Constraint

Architecture gets the attention; release mechanics are what limit how many brands a team can carry. Every brand multiplies the work of shipping.

  • Build time scales linearly. Sixteen brands is sixteen archives, sixteen signing operations, sixteen uploads. Run in sequence on one CI runner, that is most of a working day. Parallel runners and a shared SwiftPM cache bring it down; nothing removes it.
  • Review is not parallel in practice. Submitting sixteen apps at once produces correlated rejections: one reviewer’s reading of a guideline, applied across the batch, blocks the whole release. Submit a reference brand first, then the rest.
  • Version strategy has to be decided early. Lockstep versioning (every brand ships 2.14.0 the same week) is simpler to support. Independent versioning gives brands autonomy and gives you sixteen live versions to reproduce bugs against.
  • Staged rollout is per app. Phased release in App Store Connect is per listing, which is what you want: a crash in one brand’s configuration pauses that brand, not the fleet.

One parameterised lane (fastlane release brand:acme, or your CI’s matrix equivalent) that derives scheme, signing team, and App Store Connect credentials from the brand key is the whole game. Once there is a per-brand script, there will soon be sixteen slightly different ones. Teams reliably underestimate this: the architecture supports twenty brands long before the release process does.

Testing Without a Sixteen-Way Matrix

You cannot run the full suite against every brand, and you do not need to. Feature logic is identical by construction; that is the entire premise. What differs is configuration, and configuration fails in its own characteristic ways.

Split the suite in two:

  • Full suite, one reference brand. Unit, integration, and UI tests run against a single brand chosen to exercise the widest capability set.
  • Conformance suite, every brand. Fast, shallow, and purely structural: every theme token resolves, every referenced asset exists, every required configuration key is present and well-formed, the app launches and reaches its first meaningful screen, capability flags parse into a valid combination.

In practice that is a small XCTest target run once per scheme: a loop over the theme’s asset names asserting UIImage(named:) != nil, and one UI test that waits for the home screen’s accessibility identifier. It catches the failure that actually occurs in white-label programs: not broken logic, but a brand onboarded with an incomplete configuration. Snapshot tests across brands are tempting here and age badly; assert that a token resolves, not that a screen looks a particular way.

I published the runtime half of this design as an open-source Swift package, WhiteLabelKit: it reads the brand key from Info.plist, loads that brand’s configuration, exposes the theme and capability flags to SwiftUI and UIKit, and ships a BrandConformance check you can call from each scheme’s test target.

Onboarding Brand Seventeen

The honest measure of a white-label architecture is how long it takes to add a brand, and how much of that time is engineering rather than asset collection.

In a healthy setup, adding a brand means:

  • One new .xcconfig, one pair of build configurations, and one new scheme
  • One asset catalog and one theme definition under Brands/<key>/
  • One capability manifest
  • One App Store Connect record, one bundle identifier, one provisioning profile
  • One entry in the CI matrix
  • Zero changes to feature code

If that last line is not true, the architecture has a leak worth finding before the next brand arrives. The most common leak is a capability that was implemented as a brand check because it was needed the week before a launch, which is exactly when nobody has time to model it properly, and exactly why it should be caught by a module boundary instead of a code review.

When Not to Go White-Label

White-label architecture carries permanent overhead: every feature is designed twice, once for function and once for variation. That cost is worth paying when brands share a product and differ in presentation. It is not worth paying when brands are diverging products that happen to share a company.

The signal to watch is capability flags. A handful describing genuine tiers is healthy. Dozens, with combinations nobody can enumerate, means the brands stopped being one product, and the codebase is now paying white-label costs without receiving white-label benefits. The options then are to split the diverging brand into its own app built on the shared packages, or to pull the product back into line. More flags is the one option that does not work.

If you are setting this up for a new programme, a mobile architecture audit before the first listing exists is cheap compared with reversing the first two decisions afterwards.

Frequently Asked Questions

Should a white-label iOS app use one target per brand or one target configured at runtime?

Use one target per brand when each brand needs its own App Store listing, bundle identifier, and signing identity, which is the normal case. Runtime configuration suits a single app that changes appearance per logged-in tenant, not separate store listings.

How do you keep sixteen branded iOS apps from becoming sixteen codebases?

Forbid brand conditionals in feature code. Brands differ only through configuration, asset catalogs, and capability flags resolved behind a single interface, so feature code never names a brand.

How do you test a white-label app across every brand?

Run the full test suite against one reference brand, then run a lighter conformance suite against every brand that asserts configuration completeness, asset presence, and launch health rather than re-testing feature logic.

Puneetpal Arneja, Senior iOS Developer & Mobile Architect

Written by

Staff Mobile Engineer at Albertsons · 13+ years building mobile apps

I've shipped 18+ mobile applications across banking, healthcare, and consumer products — including work for Citibank, Apple, PNC, and Albertsons. Everything here comes out of production work, not theory.

Planning a Multi-Brand iOS Program?

I have shipped a single Swift codebase as sixteen production retail apps. I help teams set the configuration boundaries early, so brand seventeen costs a day instead of a quarter.

Related Services & Further Reading