Skip to main content

I built a sticker pack. Thirty fishing puns, no code, two targets — the structure Xcode generates for you when you pick “Sticker Pack App” out of the template picker. It is the smallest shippable thing on the platform.

It took eight failed uploads and most of a day to get it into App Store Connect, and the fix was to stop being a sticker pack.

Every upload came back the same way. HTTP 409, STATE_ERROR.VALIDATION_ERROR:

Missing app icon. iMessage-only apps must include an asset catalog with a large app icon to be submitted for review. Manually add a large app icon as a 1024 by 1024 pixel PNG and resubmit.

The diagnosis writes itself. Add a 1024×1024 icon. I have one — it’s sitting in the repo, it’s the icon on the App Store listing, it’s a flexing largemouth bass in aviators. The error even tells you the dimensions.

The problem is that there is nowhere to put it.

The Format Has No Slot

An iMessage sticker pack uses a .stickersiconset, not the .appiconset a normal iOS app uses. Different schema, different slots. Open Xcode 26.2’s own “Sticker Pack App” template and the entire icon inventory is:

29×29   60×45   67×50   74×55   27×20   32×24   1024×768

That’s it. The largest entry is 1024×768 — the Messages App Store banner, a 4:3 image. There is no square 1024. Not one that’s empty; one that does not exist in the schema.

So I added it by hand. actool dropped it silently — no warning, no error, just an inventory that came out the other side without it. I tried the entry as ios-marketing, as ios-marketing with an explicit platform:ios, as universal with platform:ios. Dropped every time.

Then I added a separate .appiconset named “iMessage App Icon” alongside the sticker icon set, which is roughly what the error message describes. actool exited 255 with no diagnostics at all. Not an error message — a crash.

At this point the check is asking for an artifact that the toolchain will not produce and the format has no room for.

Four Configurations, All Rejected

I stopped guessing and started enumerating. Each of these is a full archive and a full upload:

  1. Template-exact. No square 1024 anywhere, because the template has no slot for one. → 409.
  2. Host app given its own AppIcon.appiconset, modern single-size 1024 in the “Any Appearance” well. → 409.
  3. Same, in legacy multi-size format with an explicit ios-marketing 1024×1024, CFBundleIconName present. I verified the compiled Assets.car with assetutil and the opaque 1024×1024 Icon Image was genuinely in there. → 409.
  4. Everything, everywhere. ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS=YES so both the app’s and the extension’s Assets.car carried an AppIcon 1024×1024. A loose 1024×1024 PNG in both bundles. Both registered in CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconFiles. CFBundleIconName in both Info.plists. Verified with assetutil. → 409, and every other icon check passed.

Configuration 4 is the one that hurt. There is no remaining interpretation of “include a 1024×1024 app icon” that build does not satisfy.

The Configuration That Explained It

Then I did something slightly perverse. I switched the extension’s primary icon to a regular AppIcon.appiconset containing the 1024, letting actool auto-register the loose AppIcon1024x1024.png.

The response came back with nine errors instead of one. The original 1024 complaint, plus eight new ones demanding the sticker drawer renditions I had just displaced:

54×40   81×60   64×48   96×72   120×90   134×100   148×110   180×135

That’s the most useful failure I got all day. It proves the validator reads the extension’s icon inventory accurately and reports on it in detail. The server knows exactly what’s in the bundle. It is not confused about my assets. Configuration 4 satisfied every check it has except the one it wouldn’t stop raising.

What the Server Was Actually Reading

Buried in ContentDelivery.framework there’s a tool called swinfo. It’s what the upload pipeline itself runs to produce the ASSET_DESCRIPTION that the server validates against. You can run it on your own IPA and see the exact document the validator will see:

ContentDelivery.framework/Resources/swinfo -f <ipa>

When a server-side check rejects your binary, the argument isn’t really about your Xcode settings. It’s about the asset description generated from your bundle, and until you can read that document you are arguing about it blind.

For configuration 4, swinfo confirmed both bundles’ icon inventories contained a 1024×1024 entry. The thing being demanded was present in the document being validated, and the validation failed anyway.

The control that settled it: Feastmark, a normal SwiftUI app from the same team, same machine, same Xcode. It uploads fine. Its swinfo icon inventory contains no 1024×1024 either.

So the check isn’t “does this bundle have a large icon.” It’s a code path that only runs for application.messages products, and on that path it cannot be satisfied by anything Xcode 26.2 will build.

The Fix Was to Stop Qualifying

The error says iMessage-only apps must include the icon. So I made it not an iMessage-only app.

The host target went from com.apple.product-type.application.messages — the code-less stub Xcode generates — to com.apple.product-type.application, a regular iOS app. I gave it a real SwiftUI screen: a gallery that reads the sticker PNGs out of the embedded extension at runtime and displays the pack. Maybe eighty lines. The sticker extension itself I did not touch.

The next upload went through.

It isn’t a workaround for the bug so much as a way of no longer being the kind of app the bug applies to, and it isn’t free:

  • The app now has a home-screen icon, because it’s now an app.
  • The App Store Connect primary category had to become Entertainment. The Stickers category is reserved for iMessage-only apps — the exact classification I’d just abandoned.
  • The iOS screenshot slot became the one that mattered.
  • There’s now a UI to maintain on something I’d designed to have none.

For a pack of thirty fishing jokes, that’s a real amount of collateral change to route around a validator.

The Dead End I Spent Two Hours On

Between configurations 3 and 4 I convinced myself the answer was to hand-write the icon registration. swinfo said the server reads CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconFiles, so I declared it myself and dropped a loose iMessage App Icon1024x1024.png into the bundle next to it.

It doesn’t take. actool emits a partial Info.plist that gets merged over yours, and it overwrites CFBundleIcons wholesale. A static plist entry loses. A build script phase either loses too or introduces a dependency cycle trying to run late enough to win.

The only thing that survives is patching the archive after archiving, before distribution — Organizer re-signs during export, so the edit holds:

A=<archive>/Products/Applications/ReelTalk.app
/usr/libexec/PlistBuddy -c "Add :CFBundleIcons:CFBundlePrimaryIcon:CFBundleIconFiles: string 'iMessage App Icon1024x1024'" "$A/Info.plist"
cp "ReelTalk/HostAssets/iMessage App Icon1024x1024.png" "$A/"

None of which worked. That’s configuration 4 — swinfo confirmed the 1024 was in the inventory and the upload was rejected anyway. I’m keeping it here because the actool-overwrites-your-plist behavior is real and will cost somebody an evening. But I’d been handed a true fact about what the server reads, and I spent two hours using it to build a more and more elaborate answer to a question nobody had asked.

Once the host became a regular app, all of it evaporated. A normal app has a normal .appiconset, actool generates CFBundleIcons correctly on its own, and the hand-written plist block came back out of the project in the same commit that added the SwiftUI gallery. The shipping build patches nothing.

What I’d Take From This

Three of my eight uploads went into satisfying a sentence instead of asking whether the sentence could be satisfied. “Manually add a large app icon as a 1024 by 1024 pixel PNG” names a size, a format, and an action, and I took all three as facts about my project rather than as a string somebody typed into a validator at some point and nobody revisited.

An error message is a claim about your bundle. It is not evidence that the claim is checkable. The template had no square slot and actool refused to emit one. Both of those were knowable in the first ten minutes, from Xcode, without uploading anything. I went and confirmed them after the fourth rejection.

swinfo is the thing I’d reach for sooner next time. It sits in ContentDelivery.framework, it prints the exact document the server validates against, and it was there the entire time I was guessing. Once I could read that, the question stopped being what does Apple want and became what is Apple reading — and the second one you can answer in a terminal instead of by building an archive and waiting on a 409.

The change that finally shipped it is one SwiftUI file, about eighty lines, displaying a grid of stickers. Nobody who buys a sticker pack is ever going to open that screen. It exists so the binary would stop being the kind of thing the broken check runs against.

Filed with Apple Developer Support as case 20000119974759. As far as I know the stub path is still broken for everyone else.

Raul C. Peña

Raul C. Peña

Senior Software Engineer at Dell Technologies. Air Force veteran, 20+ years in Texas real estate, self-taught coder. Passionate about DevOps, IBM Vault (formerly HashiCorp Vault), and building things that matter.