Engineering Guides

Build a Reliable iOS and Mac Catalyst Validation Gate

Build a Reliable iOS and Mac Catalyst Validation Gate

When a project originally built only for iPhone and iPad enables Mac Catalyst, a common assumption is that “if it compiles for iOS, it should also compile for Mac.” In cloud Mac automation, however, failures usually stem from dependency platform declarations, resource membership, conditional compilation, and shared caches. The reliable approach is not to append another destination to an existing command, but to treat the two platforms as build products that share source code while being validated independently.

Verify That the Project Truly Supports Both Targets

First enable Mac Catalyst in the target settings in Xcode, then verify that the corresponding project-file changes have been committed. Do not rely solely on the checkbox in the graphical interface; automation should read the resolved values from the build settings:

xcodebuild \
  -project DemoApp.xcodeproj \
  -scheme DemoApp \
  -showBuildSettings |
grep -E 'SUPPORTS_MACCATALYST|PRODUCT_BUNDLE_IDENTIFIER|SDKROOT'

SUPPORTS_MACCATALYST should be YES. If the project uses a workspace, replace -project with -workspace. Next, run xcodebuild -showdestinations and confirm that the shared scheme exposes both an iOS Simulator destination and a Mac Catalyst destination. If the command line cannot find the scheme, first check whether the scheme is marked Shared instead of repeatedly changing the destination string.

Review the supported platforms of every Swift Package, binary dependency, and internal module as well. A package compiling for iOS does not mean it declares support for Mac Catalyst. For closed-source binary dependencies, verify that the XCFramework contains a Catalyst slice; if it does not, the failure may not appear until the link stage.

Isolate Caches and Result Directories

iOS and Catalyst generate different module caches, intermediate objects, and linked products. If continuous integration shares one DerivedData directory, failures may vary with job order and temporarily disappear after a cleanup. Use fixed platform-specific directories and archive the result bundles alongside them:

set -euo pipefail

xcodebuild \
  -workspace DemoApp.xcworkspace \
  -scheme DemoApp \
  -destination 'generic/platform=iOS Simulator' \
  -derivedDataPath build/derived-ios \
  -resultBundlePath build/results-ios.xcresult \
  CODE_SIGNING_ALLOWED=NO \
  build-for-testing

xcodebuild \
  -workspace DemoApp.xcworkspace \
  -scheme DemoApp \
  -destination 'platform=macOS,variant=Mac Catalyst,arch=arm64' \
  -derivedDataPath build/derived-catalyst \
  -resultBundlePath build/results-catalyst.xcresult \
  CODE_SIGNING_ALLOWED=NO \
  build-for-testing

Signing is disabled here so that the gate can focus on whether the source, dependencies, and linking are valid. Archiving and distribution signing should run in later jobs to avoid mixing platform compatibility failures with signing configuration errors in the same log.

Do not treat deleting all of ~/Library/Developer/Xcode/DerivedData as the default fix. Removing only the current job’s directory avoids disrupting other jobs on the same machine and preserves evidence from the failure.

Centralize Platform Differences Instead of Scattering Checks

Catalyst differs from iOS in its UI lifecycle, menus, window behavior, and some system capabilities. Keep conditional compilation in an adaptation layer rather than scattering targetEnvironment(macCatalyst) throughout business logic.

enum PlatformLayout {
    static var usesDesktopNavigation: Bool {
        #if targetEnvironment(macCatalyst)
        return true
        #else
        return false
        #endif
    }
}

Platform-specific APIs also require availability checks. Conditional compilation identifies the current build target, but it does not guarantee that the runtime OS version provides a given API. Define a stable interface for shared protocols first, then provide separate iOS and Catalyst implementations so unit tests can verify behavior on both platforms directly.

Resources require separate verification as well. Fonts, privacy manifests, localized resources, and data models may be included in only one product because their Target Membership was not selected. After the build, inspect the product directory and confirm that critical files are present instead of waiting for a blank interface to reveal the problem at runtime.

Check iOS Mac Catalyst
Dependencies link successfully Simulator slice available Catalyst slice available
Resource membership Present in the App bundle Present in the Catalyst App bundle
Conditional compilation Mobile branch Desktop adaptation branch
Result evidence Separate xcresult Separate xcresult

Split Failures into Diagnosable Stages

A dual-target job should not report only one generic “build failed” result. Split it into dependency resolution, unsigned compilation, unit testing, archiving, and delivery validation. Run each stage only after the previous one succeeds, and name logs by platform.

Check Four Categories of Signals First When Compilation Fails

For No such module, first inspect the dependency’s platform declarations and build target. For an architecture mismatch, check the binary slices instead of immediately adding a global excluded architecture. For an unavailable API, return to the adaptation layer and add conditional compilation and availability checks. For duplicate or missing resources, inspect Copy Bundle Resources and Target Membership.

Build scripts may also contain platform assumptions. For example, if a script hard-codes the SDK as iphoneos, a Catalyst job will read the wrong path. Scripts should use the PLATFORM_NAME, SDKROOT, and TARGET_BUILD_DIR values injected by Xcode and exit immediately for unknown platforms.

Establish a Pre-Merge Checklist

At minimum, the gate should ensure that both targets can resolve dependencies and complete unsigned compilation from clean directories, with a separate .xcresult saved for each. Unit tests for core modules should cover the platform adaptation interfaces, while behavior involving windows, menus, or drag and drop should receive dedicated Catalyst tests.

Before submitting changes, review the following in order:

  • The scheme is shared, and the command line can list both destinations;
  • iOS and Catalyst use separate DerivedData directories;
  • Dependencies explicitly support both platforms, with all required binary slices present;
  • Platform checks are centralized in the adaptation layer, with no duplicate implementations of shared logic;
  • Critical resources are included in both products;
  • Failure logs, result bundles, and build-setting snapshots are archived by platform;
  • The compilation gate and archive-signing jobs run separately.

When first implementing this workflow on G-Mini or another remote build environment, run it twice consecutively, then reverse the execution order of the two targets. If the results change with the order, investigate shared caches, temporary script directories, and generated files that were not cleaned. A stable dual-target workflow is not one that occasionally passes for both platforms; it produces consistent results from a clean workspace in any execution order.

Frequently asked questions

Should iOS and Mac Catalyst share one DerivedData directory?

They can during quick local experiments, but CI should use separate directories. Isolation prevents stale intermediates from crossing platform boundaries and makes cleanup and diagnosis more predictable.

Does the first dual-target gate need the full test suite?

No. Start with unsigned compilation and focused unit tests for both destinations, then add UI tests, archives, and signing checks after the basic platform contract is stable.

What should I inspect first when the Catalyst build fails?

Verify Catalyst support on the target, dependency platform support, conditional compilation branches, target membership for resources, and platform-specific build phases before investigating archive signing.

Exclusive physical nodes

Cloud Mac for your next build queue

Compare two M4 configurations across five nodes, then choose daily, weekly, monthly, or quarterly rental based on your workflow.

Choose a Cloud Mac plan