Skip to content
C.W.K.
Stream
Lesson 02 of 07 · published

Identifiers Come Into Existence in an Order

~15 min · testflight, app-store-connect, bundle-id, provisioning, xcodegen

Level 0Bundle Opener
0 XP0/81 lessons0/17 achievements
0/100 XP to next level100 XP to go0% complete
"An empty list plus an invitation to create one reads as 'it does not exist', which is exactly wrong."

The Archive Registers, the Record Chooses

A new iOS app needs an App ID in the team (the bundle identifier Apple knows about), and an App Store Connect record that builds upload into. The intuitive order is to create the identifier first on the developer website, then the record, then build. The family's order is the reverse, because xcodebuild archive -allowProvisioningUpdates with automatic signing registers what the project needs on its way past: the app's identifier, the share extension's, and the App Group they share. By the time anyone opens App Store Connect, the identifier already exists, and the New App screen's Bundle ID field is a dropdown to choose from.

The team itself is written into project.yml as DEVELOPMENT_TEAM, never picked in Xcode's Signing pane, because a team chosen in the UI lives in the generated project and disappears at the next xcodegen generate. Export compliance is answered in source too (ITSAppUsesNonExemptEncryption), so a build never stalls waiting for someone to answer it in a browser.

One Afternoon, Three Apps, One Wrong Turn

On one afternoon three family apps reached the record step. Each session told the owner to create a record for its bundle id. He opened the New App screen and found the Bundle ID dropdown empty, with a link beneath it offering to register a new bundle ID. An empty list plus that invitation read as "it does not exist", so he followed the link, and the developer website refused the identifier as already existing, with no reason given. That refusal reads exactly like a collision with someone else's app, so he invented a second identifier with an extra word in it and built the records against those. One session then changed its app's real bundle ids to match the workaround.

Two facts were stacked. The dropdown reflects new registrations only after a delay of minutes. And the archive had registered the identifier, so creating it by hand was never a step. The handling of an empty dropdown is to wait and reload, never to create. Recovery was cheap: a record's Bundle ID stays editable until the first build is uploaded against it, so repointing it at the real identifier fixed one app with its source untouched. After a first upload it is frozen, and the record must be deleted and made again.

How to Tell an Entry Is Yours

An identifier registered by Xcode carries Xcode's default description: the letters XC followed by the bundle id with its dots replaced by spaces. The record's Name names the record only; the name under the icon on the home screen is CFBundleDisplayName from the app's own source, and the two may differ. An extension ships inside the host app and needs no record of its own.

Code

What a session hands over for the one browser step, after an archive has run·text
App Store Connect > Apps > + > New App
  Platforms:    iOS
  Name:         Spark            (the record's name; the home-screen name is CFBundleDisplayName)
  Bundle ID:    CHOOSE com.example.spark.mobile from the dropdown. Do not create one.
                If the list is empty or missing it, wait and reload: the archive already
                registered it, and the dropdown lags behind registration.
  SKU:          spark-mobile
  User Access:  Full Access

The entry to look for reads "XC com example spark mobile - com.example.spark.mobile".
Anything described as XC plus the id with dots as spaces was registered by a build.
Read what the archive registered, from its embedded profile·bash
# What did the archive register? Read the profile embedded in the archived app.
app=build/SparkMobile-42.xcarchive/Products/Applications/SparkMobile.app
profile=$(security cms -D -i "$app/embedded.mobileprovision")

print -r -- "$profile" | plutil -extract Name raw -o - -
#   iOS Team Provisioning Profile: com.example.spark.mobile
print -r -- "$profile" | plutil -extract Entitlements.application-identifier raw -o - -
#   EXAMPLE_TEAM.com.example.spark.mobile
print -r -- "$profile" | plutil -extract 'Entitlements.com\.apple\.security\.application-groups' json -o - -
#   ["group.com.example.spark"]

# The archive is development-signed (its profile lists devices). The export step re-signs
# it for App Store Connect, which is why no distribution profile is ever made by hand.

External links

Exercise

Archive SparkMobile once with -allowProvisioningUpdates and run the inspection commands on the result. Record the profile name, the application identifier with its team prefix replaced by EXAMPLE_TEAM, and the App Group. Then write the handoff text for your own app with its real display name, bundle id and SKU, including the empty-dropdown instruction, and keep it in the repository's delivery doc.
Hint
plutil -extract needs dots inside a key escaped with backslashes, which is why the App Group key is quoted. If security cms -D prints nothing, check that the path points at the .app inside Products/Applications, not at the archive folder.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.