Skip to content
macOSDeep Dive Published Updated 9 min readViews unavailable

macOS Managed Background Assets: Pack Delivery and Version Lifecycle

Design macOS asset packs around install, prefetch, and on-demand policies, then test Apple-hosted version changes against older app builds.

The Background Assets framework predates managed packs: Apple lists the framework for macOS 13 and later and separately documents low-level APIs for apps that manage and host downloads themselves. Managed Background Assets requires macOS 26 or later. Apple-hosted delivery is additionally available to apps using Managed Background Assets on TestFlight or the App Store, on platforms other than watchOS (platform guidance; Apple-hosted setup). The main engineering decision is not simply where to download files. It is how to divide content into packs, decide when each pack must exist locally, and keep every app build compatible with the pack version currently offered to that distribution audience. This article focuses on managed packs and Apple-hosted delivery for macOS applications. It is not a guide to generic URLSession downloads, the older On-Demand Resources service, or self-hosted unmanaged Background Assets. Check the exact APIs against the Xcode SDK and minimum deployment target you ship.

Make the pack boundary follow product behavior

An asset pack is an archive of files the system downloads together. Give it a stable assetPackID that the app can resolve, then group files that share a meaningful delivery boundary: tutorial media, a level, a model bundle, or an optional language resource. Do not make one huge archive just because packaging is simpler. A pack that is too broad can force a user to download content they do not need; packs that are too fragmented increase manifest and release coordination work.

The manifest carries the identifier, download policy, file selectors, and supported platforms. Paths in its selectors are relative to the directory from which ba-package runs. A compact macOS tutorial pack can look like this:

{
  "assetPackID": "Tutorial",
  "downloadPolicy": {
    "essential": {
      "installationEventTypes": ["firstInstallation"]
    }
  },
  "fileSelectors": [
    { "directory": "Content/Tutorial" }
  ],
  "platforms": ["macOS"]
}

The firstInstallation choice is a product decision: it makes the tutorial pack part of the first install event without requesting it again for every subsequent app update. If an update changes the app so older tutorial assets cannot be used, that policy is wrong; model the update event and test the compatibility path. Generate the starter manifest with xcrun ba-package template, use the exact platform identifiers in the generated template, and package from the repository root so the relative paths resolve as intended:

xcrun ba-package Manifest.json -o Tutorial.aar

Review the archive and manifest as release inputs, not as transient build output. A typo in assetPackID, a path rooted in the wrong directory, or a platform list that excludes macOS can leave an otherwise successful app build without the required content.

Choose a download policy by launch contract

Managed packs expose three policies with different user-visible consequences:

Policy System behavior Appropriate use
essential The pack downloads as part of app installation and contributes to installation progress. Content that must be present before the first supported app use.
prefetch The system starts the download during installation, but may continue it after app installation finishes. Likely-needed content that should not block the app from becoming launchable.
onDemand The pack downloads only after the app explicitly requests it through the Background Assets API. Optional levels, large media, or content unlocked later.

For essential and prefetch, the manifest includes installationEventTypes, such as firstInstallation or subsequentUpdate. For onDemand, its policy object is empty and the application controls when it requests local availability. Do not treat prefetch as a guarantee that the pack has completed before the user launches the app. The app should check availability or request the pack before it tries to open an asset that depends on it.

The current Swift interface exposes an AssetPackManager actor for retrieving pack metadata, requesting local availability, observing status updates, accessing content, and removing unused packs. Apple’s API pages have evolved: older overloads are marked deprecated and the current documentation includes newer overloads and, in places, beta-only manifest features. Resolve the pack and request availability using the symbols supported by the exact SDK you build against, and verify their availability annotations rather than copying an old WWDC sample signature into a newer deployment target. The ensureLocalAvailability(of:requireLatestVersion:) overload is available starting with macOS 26.4. When requireLatestVersion is true, it waits for an available newer pack version to download before returning; when false (the default), it does not check for updates or attempt to download one. Choose based on whether the feature may use a locally available prior pack while an update is pending.

Before reading a file, wait for the API’s local-availability operation to succeed. url(for:) resolves a path in the shared asset namespace; it does not accept a pack identifier, and Apple says it returns a well-formed URL even when no item exists at that path. Do not persist the returned URL beyond the current process. Apple also documents url(for:) as less efficient than the content and descriptor methods, and says not to use it to block the main thread. If paths can collide across packs, use contents(at:searchingInAssetPackWithID:options:) or descriptor(for:searchingInAssetPackWithID:) to constrain the read to the intended pack. If using the descriptor API for streaming, close the returned descriptor yourself. For a large video or model, avoid eagerly copying the entire resource into application memory just to determine whether the download completed.

Wire the app and downloader extension together

Apple-hosted managed delivery still requires an app-side integration. Add the Background Download extension using Xcode’s Apple-Hosted, Managed template, share an App Group between the main app and extension, and configure the app’s Background Assets property-list keys. Apple documents BAAppGroupID, BAHasManagedAssetPacks = YES, and BAUsesAppleHosting = YES for this configuration. For Apple-hosted packs, the extension adopts StoreKit’s StoreDownloaderExtension, which inherits ManagedDownloaderExtension; self-hosted managed packs use ManagedDownloaderExtension directly. Apple documents omitting the corresponding managed extension protocol when using AssetPackManager as a programmer error. Keep signing, app-group membership, bundle identity, and the extension target aligned in every configuration that you test.

Apple-hosted packs are uploaded to App Store Connect separately from an app build and are available to apps distributed through TestFlight or the App Store on platforms other than watchOS. Apple’s overview says Managed Background Assets manages compression, and its Creating managed asset packs guide describes Apple-hosted delivery as supporting up to 200 GB of compressed assets. In App Store Connect, the separate Asset pack total limit is 200 GB per app record, shared across platforms: it sums, for each pack, the largest version eligible for TestFlight testing or App Store distribution. This is a service limit and distribution condition, not a promise that an app can download unlimited content or that every pack can be delivered outside Apple’s review and release workflow. Verify current App Store Connect limits and status before planning a large release.

Treat pack versions as a compatibility interface

An Apple-hosted pack version is not tied to one specific app build. A pack version is released into a distribution context such as the App Store or a TestFlight audience, and only one pack version can be live for each context at a time. When a new App Store pack version becomes live, existing App Store app builds can begin using that new pack; a newer app binary is not necessarily installed at the same time. This makes asset paths, file formats, and metadata a compatibility interface between independently released artifacts.

Use an expand-then-contract rollout for breaking content changes:

  1. Ship an app build that understands both the old and new asset schema, while continuing to tolerate the old pack.
  2. Upload and validate the new pack version in the appropriate TestFlight or App Store context.
  3. Confirm the target app builds can read the new paths and metadata, including versions that remain in the field.
  4. After the compatibility window and rollback plan are clear, remove old-format handling in a later app release.

For each content release, record the app build range, pack ID, pack version, target context, and review state. If TestFlight internal and external audiences need different content versions, track those contexts independently. Submitting an app and asset pack together lets App Review inspect the selected combination, but approval and readiness for distribution remain distinct release gates. Do not treat a successful upload as proof that users can already retrieve the pack.

Test availability, updates, and storage cleanup

Before TestFlight or App Store submission, test the managed flow with Apple’s local mock server from the Xcode installation. The local test path uses HTTPS, so follow Apple’s current certificate and URL-override instructions for the Mac and test app. Exercise a clean first installation, a subsequent app update, an on-demand request, an interrupted or failed pack request, and a pack update that changes file layout. Verify that the app handles a missing file as an availability or versioning problem instead of crashing during launch.

Observe the framework’s status updates and log the app build, pack ID, pack version, request outcome, and sanitized failure category. Do not log private asset URLs, user data, or full response details if they can expose credentials. A successful install event does not tell you that every later on-demand pack is available, and a stale client-side “downloaded” flag is not authoritative after the system’s pack metadata changes.

The system tracks installed packs and can update them in the background, but it does not automatically remove a pack while the app remains installed. Call the manager’s removal API when product state shows that content is no longer needed and reclaiming its storage is appropriate. Removal is a lifecycle choice: retain packs needed for offline use, and make the app able to request a removed pack again if the user returns to that content. Test the remove-and-redownload path before shipping a storage cleanup policy.

The operational advantage of managed packs is that the system handles much of the transfer and update machinery. The remaining responsibility is product-level: choose useful pack boundaries, request content before use, maintain app-to-pack compatibility across independent release contexts, and test both delivery and cleanup against the current SDK.

Related:

Sources:

Comments