Four hidden bugs. A broken iOS feed. An emergency release avoided.
The home feed went blank and ads turned white. I traced four defects across SwiftUI, an ad SDK, and bidder configuration, contained the immediate ad failure across app versions, and fixed the underlying code.
The home feed was blank. Ads were failing in different ways.
After an ad-layer refactor, users reported large empty white ad boxes. Then the home feed itself started rendering blank despite receiving its content. Visible ads also turned white intermittently, and scrolling away and back brought them back.
The app combined a SwiftUI feed with Google Ad Manager, header bidding, several SDK bidding partners, and a separate direct-sold ad server. Product, ad ops, and management were pushing for an emergency release.
I investigated the failures across the app, the SDKs, and the server-side configuration. Four separate defects were masking or imitating one another.
The content arrived. Zero-height rows stopped it appearing.
I traced the render pipeline and ruled out data loss, duplicate instances, threading, view lifecycle, and scroll geometry. Two minimal experiments isolated the failure: replacing LazyVStack with VStack made the feed render, and changing empty rows from 0pt to 0.1pt did too.
A first-paint gate held the feed until the first ad was ready. When it opened, the content and inserted ad rows reached LazyVStack together. With zero-height rows at the start, the stack built row 0 and laid out nothing. An old 400pt height cache had hidden the problem for months; removing it exposed the failure.
I gave empty ad rows a minimal non-zero height, applied after impression tracking so the layout workaround does not make an empty row count as a viewable impression.
The premium slot was receiving demand it should not receive.
I decoded bid payloads from ad request exports. A bidding partner was winning the premium slot through a catch-all mapping on a parent ad unit. That mapping routed a slot intended for direct-sold ads to the partner’s 300×250 placement.
The partner’s participation was controlled by ad-unit mapping in bidder configuration, independently of the yield groups ad ops had already checked. Looking at yield groups alone did not explain the demand reaching the slot.
The creative loaded inside a zero-size wrapper.
Safari Web Inspector showed that the partner’s creative markup declared a size of 0×0. A dump of the SDK’s internal view hierarchy revealed a loaded 300×250 creative inside the SDK’s own 0×0 wrapper. The SDK never reported a size.
The old integration opened the slot at its cached 400pt height anyway. Users saw a large white box where the ad should have been.
I changed the loader to report an ad as loaded only once its creative has a real size, and to drop a creative that still has no size after a two-second deadline.
Contain the failure without waiting for an app release.
I tested the obvious server-side change: remap the slot. It did not contain the issue because the same demand could still arrive through another placement.
I worked with ad ops to deactivate the problematic iOS placements. To cover the additional slot/banner path, I created a separate mapping to another bidder placement and disabled that placement too. This isolated the known problematic demand paths while preserving the bidder’s participation in other slots.
The affected slots contributed little value from this bidder. The targeted mitigation took effect across existing app versions without a release and covered the known problem paths pending further investigation.
The server-side mitigation made the emergency release unnecessary. The app fixes could go into the next regular release.
A blocking review concern became a controlled experiment.
A reviewer raised an important concern: could fluid creatives deadlock without reserved height? If a creative needed to be on screen to measure itself, the slot might wait forever for a size.
I used an existing A/B arm to restrict requests to fluid creatives. They reported their size while detached from the screen. The experiment resolved the blocker with observed behavior.
A disappearing row was taking the visible ad with it.
For the intermittent white ads, Web Inspector showed a live, correctly sized creative. Xcode’s view debugger showed an empty container on screen. The creative had survived; its attachment to the visible row had not.
I added targeted tracing around loads, requests, size changes, window attachment, and SwiftUI representable creation and updates. On a periodic feed refresh, SwiftUI created a replacement row. The old row’s updateUIView took the ad view back, then the old row was removed with the ad still attached. The visible replacement was left empty.
I built an ownership-aware host view. A host takes the ad only when it is not already on screen. When a host leaves the window, it hands the ad to its on-screen replacement one run-loop later. The same trace confirmed the hand-offs succeeded.
Follow the evidence across every boundary.
I kept verified facts separate from hypotheses and dropped theories when the evidence contradicted them. I compared the Android implementation to explain why the failures appeared only on iOS.
The tools included Xcode’s view debugger, Safari Web Inspector for in-app WebKit views, Google’s Ad Inspector, ad-response analysis, and an AI coding assistant for log analysis and hypothesis generation.
The temporary tracing logged no user targeting data and was removed before merge. The changes were covered by tests and written explanations of their causes.
Four defects resolved. The immediate incident contained.
I fixed the blank-feed layout failure, rejected loaded creatives with no real size, and repaired the ad-view ownership during refresh. The server-side mitigation removed the problematic demand across existing app versions. The app fixes were prepared for the next regular release.
The reviewer’s blocker was resolved with a controlled experiment. Each change went into the layer responsible for the failure: feed layout, SDK integration, view ownership, or bidder configuration.
This investigation followed the broader ad architecture work. Read how I rebuilt consent-based ad loading and integrated the providers.
Have a related problem?
iOS debugging and integrations →Bring me
the problem.
An app that needs building. A migration that needs someone to own it. Numbers that don’t make sense. A product decision your team can’t resolve. Tell me what’s happening. I’ll tell you whether I’m the right person, how I’d approach it, and what the work would cost.
Email Alex ↗alex@x91tech.com