Packaging, build configuration and distribution

Build settings and xcconfig files, schemes and environment configuration, Swift packages as dependencies, TestFlight, App Store submission and server-side deployment.

Build settings and xcconfig

// Config/Shared.xcconfig
PRODUCT_BUNDLE_IDENTIFIER = com.example.app
MARKETING_VERSION = 2.4.0
CURRENT_PROJECT_VERSION = 1
SWIFT_VERSION = 6.0
SWIFT_STRICT_CONCURRENCY = complete
ENABLE_USER_SCRIPT_SANDBOXING = YES

// Config/Debug.xcconfig
#include "Shared.xcconfig"
BUNDLE_ID_SUFFIX = .debug
API_BASE_URL = https:/$()/staging.api.example.com
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG STAGING

// Config/Release.xcconfig
#include "Shared.xcconfig"
BUNDLE_ID_SUFFIX =
API_BASE_URL = https:/$()/api.example.com
  • Split configuration into a shared file and one per environment, then set each configuration's base in the project settings.
  • Prefer compilation conditions and generated constants over reading the plist at runtime, so a missing value is a build error.
  • In an xcconfig file, // starts a comment, so a literal URL needs the https:/$()/ workaround.
  • Settings you override in the project editor silently beat the xcconfig, which is the usual reason a change appears to do nothing.

Dependencies and schemes

SourceControlBest for
Swift Package, version rangeA resolved file pins the graphMost libraries
Swift Package, branchWhatever the branch is nowA temporary fork
Local package by pathThe source tree next to the appYour own modules
XCFrameworkA binary you build and hostClosed-source or non-Swift dependencies
// a shared scheme committed to the repository is what CI runs
// App.xcodeproj/xcshareddata/xcschemes/App.xcscheme
let scheme = """
<Scheme LastUpgradeVersion="1600" version="1.7">
  <TestAction buildConfiguration="Debug">
    <Testables>
      <TestableReference skipped="NO">
        <BuildableReference BuildableIdentifier="primary"
          BlueprintName="AppTests" BuildableName="AppTests.xctest"
          ReferencedContainer="container:App.xcodeproj"/>
      </TestableReference>
    </Testables>
  </TestAction>
</Scheme>
"""

An unshared scheme lives only on the machine that created it. Commit the scheme, or CI will fail with a message about a missing destination that gives no hint about the real cause.

Distribution

# archive and export for the App Store
xcodebuild archive -project App.xcodeproj -scheme App \
  -destination "generic/platform=iOS" -archivePath build/App.xcarchive

xcodebuild -exportArchive -archivePath build/App.xcarchive \
  -exportOptionsPlist Config/ExportOptions.plist -exportPath build/ipa

# server-side Swift on Linux
swift build -c release --static-swift-stdlib
docker build -t registry.example.com/reports:1.4.0 .
docker push registry.example.com/reports:1.4.0
⚠️
Use an App Store Connect API key rather than an Apple ID password for automated uploads, and keep it out of the repository. A leaked key can publish a build on your behalf, and the upload path is the last place you want shared credentials.

FAQ

How do I stop a staging build reaching production data?
Give each environment its own bundle identifier suffix and its own base URL from the xcconfig. Two installs, two sets of preferences, and no path for a staging token to be used against production.
What is the simplest way to run Swift on a server?
A Swift package produced with swift build -c release into a small container. Static linking of the standard library keeps the image small and removes a runtime dependency from the base image.

Setting up Swift: Xcode, Swift Package Manager and Swift 6 Testing with XCTest and Swift Testing

Last refreshed 2026-09-18.