Table of Contents

  1. Apple Build System Overview
  2. App Bundles
  3. Frameworks
  4. Universal Binaries
  5. Code Signing
  6. iOS/tvOS/watchOS Cross-Compilation
  7. XCFramework Creation
  8. Xcode Project Generation
  9. Swift Integration
  10. App Store Preparation
  11. CMake and CocoaPods/SPM
  12. Debugging with LLDB
  13. Conclusion & Next Steps
Back to CMake Mastery Series

Part 31: Apple Platform Development

June 4, 2026 Wasil Zafar 35 min read

Master CMake's Xcode generator to build macOS app bundles, iOS frameworks, universal binaries, XCFrameworks, and code-signed artifacts — with Swift integration, notarization workflows, and complete App Store preparation.

Apple Build System Overview

Apple's development ecosystem revolves around Xcode as the primary IDE, but CMake can generate native Xcode projects that integrate seamlessly with Apple's tooling. The Xcode generator produces .xcodeproj files that open directly in Xcode, while the Unix Makefiles or Ninja generators work for command-line builds using the same Apple toolchain. Understanding how CMake maps to Apple's build concepts — SDKs, deployment targets, architectures, and code signing — is essential for shipping production-quality software on macOS, iOS, tvOS, watchOS, and visionOS.

# Generate an Xcode project
cmake -G Xcode -B build-xcode -S .

# Generate with Ninja (faster CI builds)
cmake -G Ninja -B build-ninja -S . \
    -DCMAKE_OSX_DEPLOYMENT_TARGET=13.0

# Query available SDKs
xcrun --show-sdk-path --sdk macosx
xcrun --show-sdk-path --sdk iphoneos
xcrun --show-sdk-path --sdk iphonesimulator

# List installed Xcode versions
xcode-select --print-path
sudo xcode-select --switch /Applications/Xcode-15.app

SDK Selection and Deployment Targets

CMake uses CMAKE_OSX_SYSROOT to select the SDK and CMAKE_OSX_DEPLOYMENT_TARGET to set the minimum OS version your binary supports. These map directly to Xcode's SDKROOT and *_DEPLOYMENT_TARGET build settings.

cmake_minimum_required(VERSION 3.25)
project(MyMacApp LANGUAGES CXX)

# SDK selection (auto-detected from xcode-select, or set explicitly)
set(CMAKE_OSX_SYSROOT macosx)  # Options: macosx, iphoneos, iphonesimulator, appletvos, watchos

# Deployment target — minimum OS version supported
set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0" CACHE STRING "Minimum macOS version")

# Verify settings
message(STATUS "SDK: ${CMAKE_OSX_SYSROOT}")
message(STATUS "Deployment Target: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
message(STATUS "Xcode version: ${XCODE_VERSION}")

add_executable(MyApp main.cpp)
target_compile_features(MyApp PRIVATE cxx_std_20)
Key Insight: Setting CMAKE_OSX_DEPLOYMENT_TARGET as a cache variable allows users to override it at configure time without modifying CMakeLists.txt. This is critical for CI pipelines that must build for different minimum OS versions from the same source tree.

App Bundles

macOS applications are distributed as .app bundles — directory structures that contain the executable, resources, frameworks, and metadata. CMake creates bundles automatically when you set the MACOSX_BUNDLE property on an executable target.

macOS App Bundle Directory Structure
        flowchart TD
            A["MyApp.app/"] --> B["Contents/"]
            B --> C["MacOS/"]
            B --> D["Resources/"]
            B --> E["Frameworks/"]
            B --> F["Info.plist"]
            B --> G["PkgInfo"]
            C --> H["MyApp (executable)"]
            D --> I["app.icns"]
            D --> J["Assets.car"]
            D --> K["en.lproj/"]
            E --> L["libfoo.dylib"]
            E --> M["MyFramework.framework/"]

            style A fill:#132440,color:#fff
            style B fill:#16476A,color:#fff
            style C fill:#3B9797,color:#fff
            style D fill:#3B9797,color:#fff
            style E fill:#3B9797,color:#fff
    
cmake_minimum_required(VERSION 3.25)
project(MyMacApp LANGUAGES CXX)

set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0")

# Create app bundle
add_executable(MyMacApp MACOSX_BUNDLE
    src/main.cpp
    src/AppDelegate.cpp
)

# Configure Info.plist from template
set_target_properties(MyMacApp PROPERTIES
    MACOSX_BUNDLE TRUE
    MACOSX_BUNDLE_BUNDLE_NAME "My Mac App"
    MACOSX_BUNDLE_BUNDLE_VERSION "1.0.0"
    MACOSX_BUNDLE_SHORT_VERSION_STRING "1.0"
    MACOSX_BUNDLE_GUI_IDENTIFIER "com.example.mymacapp"
    MACOSX_BUNDLE_COPYRIGHT "Copyright © 2026 Example Inc."
    MACOSX_BUNDLE_ICON_FILE "app.icns"
    MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist.in"
)

# Copy icon into Resources
set(APP_ICON "${CMAKE_CURRENT_SOURCE_DIR}/resources/app.icns")
set_source_files_properties(${APP_ICON} PROPERTIES
    MACOSX_PACKAGE_LOCATION "Resources"
)
target_sources(MyMacApp PRIVATE ${APP_ICON})

Resource Copying

App bundles need various resources — icons, XIB/storyboard files, asset catalogs, localization bundles, and data files. CMake provides multiple mechanisms to place files in the correct bundle subdirectory.

# Method 1: MACOSX_PACKAGE_LOCATION property (per source file)
set(RESOURCE_FILES
    resources/app.icns
    resources/default.png
    resources/config.json
)
set_source_files_properties(${RESOURCE_FILES} PROPERTIES
    MACOSX_PACKAGE_LOCATION "Resources"
)
target_sources(MyMacApp PRIVATE ${RESOURCE_FILES})

# Method 2: Localized resources
set(EN_RESOURCES resources/en.lproj/MainMenu.xib)
set_source_files_properties(${EN_RESOURCES} PROPERTIES
    MACOSX_PACKAGE_LOCATION "Resources/en.lproj"
)
target_sources(MyMacApp PRIVATE ${EN_RESOURCES})

# Method 3: Post-build copy for dynamic content
add_custom_command(TARGET MyMacApp POST_BUILD
    COMMAND ${CMAKE_COMMAND} -E copy_directory
        "${CMAKE_CURRENT_SOURCE_DIR}/assets"
        "$<TARGET_BUNDLE_CONTENT_DIR:MyMacApp>/Resources/assets"
    COMMENT "Copying asset directory into bundle"
)

# Method 4: install(TARGETS) with BUNDLE DESTINATION
install(TARGETS MyMacApp
    BUNDLE DESTINATION .
    COMPONENT Runtime
)
Apple Gotcha: Asset catalogs (.xcassets) require compilation by actool and are only properly handled by the Xcode generator. If you use Ninja or Makefiles, you must invoke actool manually via add_custom_command(). For production apps, prefer the Xcode generator for asset catalog support.

Frameworks

Apple frameworks are versioned bundles that package a dynamic library with its public headers, resources, and metadata. CMake builds frameworks by setting the FRAMEWORK property on a library target, producing a proper .framework bundle that other projects can link against.

cmake_minimum_required(VERSION 3.25)
project(MyFramework LANGUAGES CXX)

set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0")

# Define the framework library
add_library(MyFramework SHARED
    src/Core.cpp
    src/Networking.cpp
    src/Utilities.cpp
)

# Mark as framework and configure properties
set_target_properties(MyFramework PROPERTIES
    FRAMEWORK TRUE
    FRAMEWORK_VERSION "A"
    MACOSX_FRAMEWORK_IDENTIFIER "com.example.MyFramework"
    MACOSX_FRAMEWORK_BUNDLE_VERSION "1.0.0"
    MACOSX_FRAMEWORK_SHORT_VERSION_STRING "1.0"
    # Public headers go into Headers/ inside the framework
    PUBLIC_HEADER "include/MyFramework/Core.h;include/MyFramework/Networking.h;include/MyFramework/Utilities.h"
    # Private headers (available to framework internals)
    PRIVATE_HEADER "src/Internal.h"
    # Framework resources
    RESOURCE "resources/config.plist"
    # Version compatibility
    VERSION "1.0.0"
    SOVERSION "1"
)

# Include directories for consumers
target_include_directories(MyFramework
    PUBLIC
        $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
        $<INSTALL_INTERFACE:MyFramework.framework/Headers>
)

# Install the framework
install(TARGETS MyFramework
    FRAMEWORK DESTINATION "Library/Frameworks"
    COMPONENT Development
)

Versioning and Umbrella Headers

Apple frameworks support versioning through a symlink-based directory structure. The umbrella header is a single header that includes all public headers, making it easy for consumers to import the entire framework.

// include/MyFramework/MyFramework.h — Umbrella header
#ifndef MYFRAMEWORK_H
#define MYFRAMEWORK_H

#include <MyFramework/Core.h>
#include <MyFramework/Networking.h>
#include <MyFramework/Utilities.h>

#endif // MYFRAMEWORK_H
# Module map for Swift interop and @import support
file(WRITE "${CMAKE_CURRENT_BINARY_DIR}/module.modulemap"
"framework module MyFramework {
    umbrella header \"MyFramework.h\"
    export *
    module * { export * }
}")

# Copy module map into framework
set_source_files_properties(
    "${CMAKE_CURRENT_BINARY_DIR}/module.modulemap"
    PROPERTIES MACOSX_PACKAGE_LOCATION "Modules"
)
target_sources(MyFramework PRIVATE
    "${CMAKE_CURRENT_BINARY_DIR}/module.modulemap"
)

Universal Binaries

Universal binaries (fat binaries) contain machine code for multiple CPU architectures in a single file. With Apple's transition from Intel (x86_64) to Apple Silicon (arm64), universal binaries ensure apps run natively on both architectures without Rosetta 2 translation.

Universal Binary Creation Flow
        flowchart LR
            A[Source Code] --> B[arm64 Build]
            A --> C[x86_64 Build]
            B --> D[arm64 Object Files]
            C --> E[x86_64 Object Files]
            D --> F[arm64 Binary]
            E --> G[x86_64 Binary]
            F --> H["lipo -create"]
            G --> H
            H --> I[Universal Binary
arm64 + x86_64] style H fill:#BF092F,color:#fff style I fill:#132440,color:#fff
cmake_minimum_required(VERSION 3.25)
project(UniversalApp LANGUAGES CXX)

# Build universal binary with both architectures
set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64" CACHE STRING "Target architectures")

# Deployment target must support both architectures
set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0")

add_executable(UniversalApp src/main.cpp)

# Verify architecture at configure time
message(STATUS "Building for architectures: ${CMAKE_OSX_ARCHITECTURES}")

# Per-architecture compile flags (generator expressions)
target_compile_options(UniversalApp PRIVATE
    # ARM64-specific optimizations
    $<$<STREQUAL:${CMAKE_OSX_ARCHITECTURES},arm64>:-mcpu=apple-m1>
)

Working with lipo

When CMake's built-in universal binary support is insufficient — for example, when combining separately-built architectures from different build trees — use Apple's lipo tool directly.

# Inspect architectures in a binary
lipo -info build/Release/MyApp
# Output: Architectures in the fat file: MyApp are: x86_64 arm64

# Create universal binary from separate builds
cmake -B build-arm64 -S . -DCMAKE_OSX_ARCHITECTURES=arm64
cmake --build build-arm64 --config Release

cmake -B build-x86 -S . -DCMAKE_OSX_ARCHITECTURES=x86_64
cmake --build build-x86 --config Release

# Combine with lipo
lipo -create \
    build-arm64/Release/MyApp \
    build-x86/Release/MyApp \
    -output MyApp-universal

# Verify the result
lipo -info MyApp-universal
file MyApp-universal

# Extract a single architecture
lipo MyApp-universal -thin arm64 -output MyApp-arm64
Key Insight: With the Xcode generator, setting CMAKE_OSX_ARCHITECTURES to "arm64;x86_64" automatically produces universal binaries without needing to run lipo manually. The Ninja and Makefiles generators also support this natively on CMake 3.19+.

Code Signing

All software distributed on Apple platforms must be code-signed. macOS requires signing for notarization (Gatekeeper), while iOS/tvOS/watchOS require signing for device deployment. CMake configures code signing through Xcode build settings attributes.

cmake_minimum_required(VERSION 3.25)
project(SignedApp LANGUAGES CXX)

add_executable(SignedApp MACOSX_BUNDLE src/main.cpp)

# Code signing configuration via Xcode attributes
set_target_properties(SignedApp PROPERTIES
    XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Development"
    XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "ABCDE12345"
    XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Automatic"
    XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER ""
)

# For distribution builds (App Store or Developer ID)
# set_target_properties(SignedApp PROPERTIES
#     XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Developer ID Application"
#     XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Manual"
# )

# Enable hardened runtime (required for notarization)
set_target_properties(SignedApp PROPERTIES
    XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES
)

Entitlements

Entitlements declare specific capabilities your app requires — network access, file access, camera, etc. They are specified in a .entitlements plist file and embedded during code signing.

# Point to entitlements file
set_target_properties(SignedApp PROPERTIES
    XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS
        "${CMAKE_CURRENT_SOURCE_DIR}/SignedApp.entitlements"
)

# For command-line signing (non-Xcode generator)
add_custom_command(TARGET SignedApp POST_BUILD
    COMMAND codesign --force --sign "Developer ID Application: My Company (ABCDE12345)"
        --entitlements "${CMAKE_CURRENT_SOURCE_DIR}/SignedApp.entitlements"
        --options runtime
        --timestamp
        "$<TARGET_BUNDLE_DIR:SignedApp>"
    COMMENT "Code signing SignedApp.app"
)
# SignedApp.entitlements — example plist content
# Typically created via Xcode or manually:
cat << 'EOF' > SignedApp.entitlements
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>com.apple.security.app-sandbox</key>
    <true/>
    <key>com.apple.security.network.client</key>
    <true/>
    <key>com.apple.security.files.user-selected.read-write</key>
    <true/>
</dict>
</plist>
EOF
Apple Gotcha: Hardened runtime is mandatory for notarization since macOS 10.15. If your app uses JIT compilation, dlopen() for plugins, or unsigned memory mapping, you must declare the corresponding entitlements (com.apple.security.cs.allow-jit, com.apple.security.cs.disable-library-validation, etc.) or the app will crash at runtime.

iOS/tvOS/watchOS Cross-Compilation

Building for iOS and other embedded Apple platforms requires cross-compilation. CMake handles this by setting CMAKE_SYSTEM_NAME to the target platform, which triggers platform-specific behavior including SDK resolution, architecture selection, and signing requirements.

# ios-toolchain.cmake — CMake toolchain file for iOS
set(CMAKE_SYSTEM_NAME iOS)
set(CMAKE_OSX_DEPLOYMENT_TARGET "16.0")

# Device build (default)
set(CMAKE_OSX_SYSROOT iphoneos)
set(CMAKE_OSX_ARCHITECTURES arm64)

# Or use the variable to switch between device and simulator:
# set(CMAKE_OSX_SYSROOT iphonesimulator)
# set(CMAKE_OSX_ARCHITECTURES "arm64;x86_64")  # Simulator needs both for M1 + Intel Macs

# Compiler flags for iOS
set(CMAKE_C_FLAGS_INIT "-fembed-bitcode")
set(CMAKE_CXX_FLAGS_INIT "-fembed-bitcode")

# Skip compiler tests that fail during cross-compilation
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
# Build for iOS device
cmake -B build-ios -S . \
    -G Xcode \
    -DCMAKE_TOOLCHAIN_FILE=ios-toolchain.cmake \
    -DCMAKE_SYSTEM_NAME=iOS \
    -DCMAKE_OSX_DEPLOYMENT_TARGET=16.0

# Build for iOS Simulator
cmake -B build-sim -S . \
    -G Xcode \
    -DCMAKE_SYSTEM_NAME=iOS \
    -DCMAKE_OSX_SYSROOT=iphonesimulator \
    -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"

# Build using xcodebuild (after CMake generates the project)
xcodebuild -project build-ios/MyApp.xcodeproj \
    -scheme MyApp \
    -sdk iphoneos \
    -configuration Release \
    -destination "generic/platform=iOS"

Simulator vs Device Builds

A common CI pattern builds libraries for both simulator and device, then merges them into an XCFramework. CMake 3.21+ simplifies this with improved iOS support.

cmake_minimum_required(VERSION 3.25)
project(MyIOSLib LANGUAGES CXX)

# Detect platform from CMAKE_SYSTEM_NAME
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
    message(STATUS "Building for iOS")
    message(STATUS "  SDK: ${CMAKE_OSX_SYSROOT}")
    message(STATUS "  Architectures: ${CMAKE_OSX_ARCHITECTURES}")
    message(STATUS "  Deployment target: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif()

add_library(MyIOSLib STATIC
    src/networking.cpp
    src/storage.cpp
    src/crypto.cpp
)

target_include_directories(MyIOSLib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

# iOS-specific: disable exceptions if needed for size
if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
    target_compile_options(MyIOSLib PRIVATE -fno-exceptions -fno-rtti)
endif()

install(TARGETS MyIOSLib
    ARCHIVE DESTINATION lib
    PUBLIC_HEADER DESTINATION include/MyIOSLib
)
Real-World Example
Building a Cross-Platform Library for iOS and macOS

A networking library that compiles for macOS (arm64 + x86_64), iOS device (arm64), and iOS Simulator (arm64 + x86_64). The CI pipeline builds all variants and produces a single XCFramework.

# CI script: build all platforms
#!/bin/bash
set -e

# macOS universal
cmake -B build-macos -S . -G Ninja \
    -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
    -DCMAKE_OSX_DEPLOYMENT_TARGET=13.0
cmake --build build-macos --config Release

# iOS device
cmake -B build-ios -S . -G Ninja \
    -DCMAKE_SYSTEM_NAME=iOS \
    -DCMAKE_OSX_SYSROOT=iphoneos \
    -DCMAKE_OSX_ARCHITECTURES=arm64 \
    -DCMAKE_OSX_DEPLOYMENT_TARGET=16.0
cmake --build build-ios --config Release

# iOS simulator (universal for M1 + Intel Macs)
cmake -B build-sim -S . -G Ninja \
    -DCMAKE_SYSTEM_NAME=iOS \
    -DCMAKE_OSX_SYSROOT=iphonesimulator \
    -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
    -DCMAKE_OSX_DEPLOYMENT_TARGET=16.0
cmake --build build-sim --config Release
iOS macOS Cross-Platform CI/CD

XCFramework Creation

XCFrameworks are Apple's modern distribution format for binary libraries. They bundle multiple platform variants (macOS, iOS device, iOS Simulator, Mac Catalyst) into a single package with proper architecture separation — solving the "duplicate architecture" problem that plagued universal frameworks when Macs and iPhone Simulators both use arm64.

XCFramework Build Pipeline
        flowchart TD
            A[Source Code] --> B[macOS Build
arm64 + x86_64] A --> C[iOS Device Build
arm64] A --> D[iOS Simulator Build
arm64 + x86_64] B --> E[libMyLib.a
macOS universal] C --> F[libMyLib.a
iOS device] D --> G[libMyLib.a
iOS simulator] E --> H["xcodebuild -create-xcframework"] F --> H G --> H H --> I["MyLib.xcframework/"] I --> J["macos-arm64_x86_64/"] I --> K["ios-arm64/"] I --> L["ios-arm64_x86_64-simulator/"] style H fill:#BF092F,color:#fff style I fill:#132440,color:#fff
# After building all platform variants (see iOS section above):

# Create XCFramework from static libraries
xcodebuild -create-xcframework \
    -library build-macos/Release/libMyLib.a \
    -headers include/ \
    -library build-ios/Release/libMyLib.a \
    -headers include/ \
    -library build-sim/Release/libMyLib.a \
    -headers include/ \
    -output MyLib.xcframework

# Create XCFramework from frameworks
xcodebuild -create-xcframework \
    -framework build-macos/Release/MyLib.framework \
    -framework build-ios/Release/MyLib.framework \
    -framework build-sim/Release/MyLib.framework \
    -output MyLib.xcframework

# Verify the result
ls MyLib.xcframework/
# Info.plist
# ios-arm64/
# ios-arm64_x86_64-simulator/
# macos-arm64_x86_64/
# CMake function to automate XCFramework creation
function(create_xcframework TARGET)
    set(options "")
    set(oneValueArgs OUTPUT_NAME)
    set(multiValueArgs PLATFORMS)
    cmake_parse_arguments(XCF "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})

    set(XCF_ARGS "")
    foreach(platform ${XCF_PLATFORMS})
        list(APPEND XCF_ARGS
            -library "${CMAKE_BINARY_DIR}/${platform}/lib${TARGET}.a"
            -headers "${CMAKE_CURRENT_SOURCE_DIR}/include"
        )
    endforeach()

    add_custom_target(${TARGET}-xcframework ALL
        COMMAND xcodebuild -create-xcframework
            ${XCF_ARGS}
            -output "${CMAKE_BINARY_DIR}/${XCF_OUTPUT_NAME}.xcframework"
        COMMENT "Creating ${XCF_OUTPUT_NAME}.xcframework"
    )
endfunction()

# Usage:
# create_xcframework(MyLib
#     OUTPUT_NAME MyLib
#     PLATFORMS macos ios ios-simulator
# )
Key Insight: XCFrameworks solve the fundamental problem of arm64 appearing in both iOS device and iOS Simulator slices. Unlike fat frameworks, each platform variant lives in its own directory, so there is no architecture collision. Always prefer XCFrameworks over universal (fat) frameworks for distribution.

Xcode Project Generation

CMake's Xcode generator creates fully-featured .xcodeproj files with build configurations, schemes, and proper target dependencies. CMake 3.15+ added CMAKE_XCODE_GENERATE_SCHEME for automatic scheme creation, eliminating the need to manually configure schemes after generation.

cmake_minimum_required(VERSION 3.25)
project(XcodeApp LANGUAGES CXX)

# Enable automatic scheme generation
set(CMAKE_XCODE_GENERATE_SCHEME TRUE)

# Set Xcode-specific build settings
set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LANGUAGE_STANDARD "c++20")
set(CMAKE_XCODE_ATTRIBUTE_CLANG_CXX_LIBRARY "libc++")
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_UNINITIALIZED_AUTOS "YES_AGGRESSIVE")
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_UNUSED_FUNCTION "YES")
set(CMAKE_XCODE_ATTRIBUTE_CLANG_WARN_SUSPICIOUS_MOVE "YES")

add_executable(XcodeApp MACOSX_BUNDLE src/main.cpp)

# Scheme environment variables (available in Xcode Run action)
set_target_properties(XcodeApp PROPERTIES
    XCODE_GENERATE_SCHEME TRUE
    XCODE_SCHEME_ENVIRONMENT "MY_CONFIG_PATH=/tmp/config.json"
    XCODE_SCHEME_ARGUMENTS "--verbose;--port=8080"
    XCODE_SCHEME_WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
)

# Address Sanitizer configuration for Debug scheme
set_target_properties(XcodeApp PROPERTIES
    XCODE_SCHEME_ADDRESS_SANITIZER TRUE
    XCODE_SCHEME_ADDRESS_SANITIZER_USE_AFTER_RETURN TRUE
    XCODE_SCHEME_UNDEFINED_BEHAVIOUR_SANITIZER TRUE
)

Test Targets

Test targets in Xcode need proper scheme configuration to appear in Xcode's test navigator and run via xcodebuild test.

# Enable testing
enable_testing()

# Test executable
add_executable(XcodeAppTests
    tests/test_main.cpp
    tests/test_networking.cpp
    tests/test_storage.cpp
)

target_link_libraries(XcodeAppTests PRIVATE
    XcodeAppLib  # library under test
    GTest::gtest
    GTest::gtest_main
)

# Register with CTest
add_test(NAME UnitTests COMMAND XcodeAppTests)

# Configure test scheme
set_target_properties(XcodeAppTests PROPERTIES
    XCODE_GENERATE_SCHEME TRUE
    XCODE_SCHEME_ADDRESS_SANITIZER TRUE
    XCODE_SCHEME_THREAD_SANITIZER TRUE
)

# Build and test from command line
# xcodebuild test -project build/XcodeApp.xcodeproj \
#     -scheme XcodeAppTests \
#     -destination "platform=macOS"

Swift Integration

CMake 3.15+ supports Swift as a first-class language. You can build pure Swift projects, mixed C++/Swift projects, and even interoperate between C++ and Swift using bridging headers or Swift's C++ interop feature (Swift 5.9+).

cmake_minimum_required(VERSION 3.25)
project(SwiftApp LANGUAGES Swift)

# Pure Swift application
add_executable(SwiftApp
    Sources/main.swift
    Sources/AppDelegate.swift
    Sources/ViewModel.swift
)

# Swift language version
set_target_properties(SwiftApp PROPERTIES
    Swift_LANGUAGE_VERSION 5.9
    MACOSX_BUNDLE TRUE
    MACOSX_BUNDLE_GUI_IDENTIFIER "com.example.swiftapp"
)

# Swift compiler flags
target_compile_options(SwiftApp PRIVATE
    -strict-concurrency=complete
    -enable-upcoming-feature ExistentialAny
)
# Mixed C++ and Swift project
cmake_minimum_required(VERSION 3.25)
project(MixedApp LANGUAGES CXX Swift)

# C++ library with headers exposed to Swift
add_library(CppCore STATIC
    cpp/Engine.cpp
    cpp/Physics.cpp
)
target_include_directories(CppCore PUBLIC cpp/include)

# Swift module that uses C++ code via bridging header
add_executable(MixedApp
    swift/main.swift
    swift/SwiftUI_Views.swift
)

# Link C++ library
target_link_libraries(MixedApp PRIVATE CppCore)

# Bridging header for Swift -> C interop
set_target_properties(MixedApp PROPERTIES
    XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER
        "${CMAKE_CURRENT_SOURCE_DIR}/swift/BridgingHeader.h"
)

# Enable C++ interop (Swift 5.9+)
target_compile_options(MixedApp PRIVATE
    $<$<COMPILE_LANGUAGE:Swift>:-cxx-interoperability-mode=default>
)

Bridging Headers

Bridging headers expose C and Objective-C APIs to Swift code. For C++ interop, Swift 5.9 introduced native C++ interoperability that avoids the Objective-C++ bridging layer entirely.

// swift/BridgingHeader.h — Expose C/ObjC APIs to Swift
#ifndef BridgingHeader_h
#define BridgingHeader_h

// C functions
#include "Engine.h"
#include "Physics.h"

// Objective-C classes (if any)
// #import "LegacyManager.h"

#endif
// swift/main.swift — Using C++ types from Swift (Swift 5.9+ interop)
import CppCore  // Module name matches the CMake target

func runSimulation() {
    // Direct C++ class usage (no bridging header needed with -cxx-interoperability-mode)
    var engine = Engine()
    engine.initialize(width: 1920, height: 1080)
    
    let physics = Physics(gravity: 9.81)
    physics.step(deltaTime: 0.016)
    
    print("Simulation frame: \(engine.frameCount)")
}
Apple Gotcha: Swift support in CMake currently works best with the Xcode generator. The Ninja generator has limited Swift support — it cannot handle incremental Swift compilation or mixed-language targets as well as Xcode. For production Swift projects, always use -G Xcode.

App Store Preparation

Distributing through the Mac App Store or direct distribution with notarization requires specific build configurations, code signing identities, and post-build workflows. CMake can automate most of this pipeline.

cmake_minimum_required(VERSION 3.25)
project(ProductionApp LANGUAGES CXX)

add_executable(ProductionApp MACOSX_BUNDLE
    src/main.cpp
    src/AppDelegate.cpp
)

# Production signing configuration
set_target_properties(ProductionApp PROPERTIES
    MACOSX_BUNDLE_BUNDLE_NAME "Production App"
    MACOSX_BUNDLE_BUNDLE_VERSION "2.1.0"
    MACOSX_BUNDLE_SHORT_VERSION_STRING "2.1"
    MACOSX_BUNDLE_GUI_IDENTIFIER "com.example.productionapp"
    # App Store distribution
    XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "Apple Distribution"
    XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "ABCDE12345"
    XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Manual"
    XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER "ProductionApp AppStore"
    # Required for App Store
    XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME YES
    XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.example.productionapp"
    # App category
    XCODE_ATTRIBUTE_INFOPLIST_KEY_LSApplicationCategoryType "public.app-category.developer-tools"
)

Archive Builds and Notarization

For distribution outside the App Store (Developer ID), Apple requires notarization — uploading your signed app to Apple's servers for malware scanning, then stapling the notarization ticket to the binary.

# Step 1: Build an archive via xcodebuild
xcodebuild archive \
    -project build/ProductionApp.xcodeproj \
    -scheme ProductionApp \
    -configuration Release \
    -archivePath build/ProductionApp.xcarchive \
    CODE_SIGN_IDENTITY="Developer ID Application: My Company (ABCDE12345)" \
    DEVELOPMENT_TEAM=ABCDE12345

# Step 2: Export from archive
xcodebuild -exportArchive \
    -archivePath build/ProductionApp.xcarchive \
    -exportPath build/export \
    -exportOptionsPlist ExportOptions.plist

# Step 3: Create DMG for distribution
hdiutil create -volname "Production App" \
    -srcfolder build/export/ProductionApp.app \
    -ov -format UDZO \
    build/ProductionApp.dmg

# Step 4: Notarize the DMG
xcrun notarytool submit build/ProductionApp.dmg \
    --apple-id "dev@example.com" \
    --team-id "ABCDE12345" \
    --password "@keychain:AC_PASSWORD" \
    --wait

# Step 5: Staple the ticket
xcrun stapler staple build/ProductionApp.dmg

# Verify notarization
spctl --assess --verbose=4 --type execute build/export/ProductionApp.app
# ExportOptions.plist for Developer ID distribution
cat << 'EOF' > ExportOptions.plist
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>method</key>
    <string>developer-id</string>
    <key>teamID</key>
    <string>ABCDE12345</string>
    <key>signingStyle</key>
    <string>manual</string>
    <key>signingCertificate</key>
    <string>Developer ID Application</string>
</dict>
</plist>
EOF
Real-World Example
Complete CI Pipeline: Build, Sign, Notarize, and Distribute

A GitHub Actions workflow that builds a CMake macOS app, signs it with Developer ID, notarizes it with Apple, and uploads the stapled DMG as a release artifact.

# .github/workflows/macos-release.yml (relevant steps)
# After checkout, install certificates, and provisioning profiles:

# Configure CMake with Xcode generator
cmake -B build -G Xcode \
    -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
    -DCMAKE_OSX_DEPLOYMENT_TARGET=13.0 \
    -DCMAKE_BUILD_TYPE=Release

# Build release configuration
cmake --build build --config Release

# Sign, notarize, staple (using custom script)
./scripts/notarize.sh build/Release/ProductionApp.app

# Create DMG and upload
hdiutil create -volname "My App" \
    -srcfolder build/Release/ProductionApp.app \
    -ov -format UDZO ProductionApp-$(git describe --tags).dmg
CI/CD Notarization GitHub Actions Release

CMake and CocoaPods/SPM

Apple's ecosystem has its own package managers — CocoaPods and Swift Package Manager (SPM). While CMake operates independently, you can integrate dependencies from these ecosystems into CMake-based projects.

# Strategy 1: Use find_package() for system-installed frameworks
# Many CocoaPods eventually install into standard paths
find_library(ALAMOFIRE_LIB Alamofire
    PATHS "${CMAKE_CURRENT_SOURCE_DIR}/Pods/Alamofire/lib"
)

# Strategy 2: FetchContent for SPM-compatible packages
include(FetchContent)

# Many C/C++ libraries have both CMake and SPM support
FetchContent_Declare(
    swift-argument-parser
    GIT_REPOSITORY https://github.com/apple/swift-argument-parser.git
    GIT_TAG 1.3.0
)
FetchContent_MakeAvailable(swift-argument-parser)

# Strategy 3: ExternalProject for CocoaPods
include(ExternalProject)
ExternalProject_Add(pods
    DOWNLOAD_COMMAND ""
    CONFIGURE_COMMAND pod install --project-directory=${CMAKE_SOURCE_DIR}
    BUILD_COMMAND ""
    INSTALL_COMMAND ""
    BUILD_IN_SOURCE TRUE
)
# Practical approach: Pre-built XCFrameworks from CocoaPods/SPM
# Most production projects pre-build dependencies and check in XCFrameworks

# Find pre-built XCFramework
find_library(REALM_LIB Realm
    PATHS "${CMAKE_CURRENT_SOURCE_DIR}/Frameworks/Realm.xcframework/macos-arm64_x86_64"
)

# Or use a CMake wrapper
add_library(Realm SHARED IMPORTED)
set_target_properties(Realm PROPERTIES
    IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/Frameworks/Realm.xcframework/macos-arm64_x86_64/Realm.framework/Realm"
    INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/Frameworks/Realm.xcframework/macos-arm64_x86_64/Realm.framework/Headers"
)

target_link_libraries(MyApp PRIVATE Realm)
Key Insight: The cleanest integration pattern is to build dependencies separately using their native build system (CocoaPods/SPM/Bazel), export them as XCFrameworks, and consume those XCFrameworks in your CMake project via IMPORTED targets. This avoids fighting with incompatible build system assumptions.

Debugging with LLDB

Apple's debugger (LLDB) requires proper debug information format configuration. CMake controls whether debug symbols are embedded in the binary (DWARF) or split into separate symbol files (dSYM bundles), which affects debugging experience, binary size, and crash report symbolication.

cmake_minimum_required(VERSION 3.25)
project(DebuggableApp LANGUAGES CXX)

add_executable(DebuggableApp src/main.cpp)

# Debug information format via Xcode attributes
set_target_properties(DebuggableApp PROPERTIES
    # DWARF with dSYM — symbols in separate .dSYM bundle (recommended for release)
    XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT "dwarf-with-dsym"
    # Or: DWARF — symbols embedded in binary (faster debug builds)
    # XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT "dwarf"
)

# Per-configuration: embedded for Debug, dSYM for Release
set_target_properties(DebuggableApp PROPERTIES
    XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT[variant=Debug] "dwarf"
    XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT[variant=Release] "dwarf-with-dsym"
)

# Generate dSYM for non-Xcode generators (Ninja/Makefiles)
if(NOT CMAKE_GENERATOR MATCHES "Xcode")
    add_custom_command(TARGET DebuggableApp POST_BUILD
        COMMAND dsymutil "$<TARGET_FILE:DebuggableApp>"
            -o "$<TARGET_FILE:DebuggableApp>.dSYM"
        COMMENT "Generating dSYM for DebuggableApp"
    )
endif()
# Generate dSYM manually
dsymutil build/Release/DebuggableApp -o build/Release/DebuggableApp.dSYM

# Verify dSYM contains symbols
dwarfdump --uuid build/Release/DebuggableApp
dwarfdump --uuid build/Release/DebuggableApp.dSYM

# Symbolicate a crash log
atos -o build/Release/DebuggableApp.dSYM -l 0x100000000 -arch arm64 0x100003f40

# Upload symbols to crash reporting service
# (Firebase Crashlytics, Sentry, etc.)
# Many services accept the .dSYM bundle directly

# Debug with LLDB from command line
lldb build/Debug/DebuggableApp
# (lldb) breakpoint set --name main
# (lldb) run
# (lldb) bt  (backtrace)
# (lldb) frame variable  (show local variables)
Apple Gotcha: When using the Ninja generator on macOS, debug symbols default to DWARF embedded in the binary. You must manually run dsymutil to generate dSYM bundles for crash report symbolication. The Xcode generator handles this automatically when DEBUG_INFORMATION_FORMAT is set to dwarf-with-dsym. Always archive dSYM bundles alongside release builds for future crash analysis.
# Complete debug configuration for production
set_target_properties(DebuggableApp PROPERTIES
    # Strip debug symbols from release binary (symbols are in dSYM)
    XCODE_ATTRIBUTE_DEPLOYMENT_POSTPROCESSING[variant=Release] "YES"
    XCODE_ATTRIBUTE_STRIP_INSTALLED_PRODUCT[variant=Release] "YES"
    XCODE_ATTRIBUTE_STRIP_STYLE "non-global"
    # Keep symbols for Debug builds
    XCODE_ATTRIBUTE_DEPLOYMENT_POSTPROCESSING[variant=Debug] "NO"
    XCODE_ATTRIBUTE_GCC_GENERATE_DEBUGGING_SYMBOLS "YES"
    # Optimization levels
    XCODE_ATTRIBUTE_GCC_OPTIMIZATION_LEVEL[variant=Debug] "0"
    XCODE_ATTRIBUTE_GCC_OPTIMIZATION_LEVEL[variant=Release] "s"
)

Conclusion and Next Steps

Apple platform development with CMake requires understanding the unique aspects of Apple's ecosystem — app bundles, frameworks, code signing, and multi-architecture builds. The key takeaways from this article:

Summary of Key Concepts:
  • Xcode Generator — produces native .xcodeproj files with full scheme support, asset catalog compilation, and Swift integration
  • App BundlesMACOSX_BUNDLE property creates proper .app structures with Info.plist and resource copying
  • FrameworksFRAMEWORK property builds versioned framework bundles with public headers and module maps
  • Universal BinariesCMAKE_OSX_ARCHITECTURES builds fat binaries for arm64 + x86_64
  • Code SigningXCODE_ATTRIBUTE_* properties configure identity, team, entitlements, and hardened runtime
  • iOS Cross-CompilationCMAKE_SYSTEM_NAME=iOS with proper sysroot and architecture settings
  • XCFrameworks — the modern distribution format that solves architecture collisions across platforms
  • Swift — CMake 3.15+ supports Swift with enable_language(Swift) and C++ interop via bridging headers
  • Notarization — mandatory for macOS distribution, automated via notarytool and stapler
  • dSYM — always generate and archive dSYM bundles for release builds to enable crash symbolication

In the next article, we will explore how to use CMake to build and distribute Python extension modules using pybind11 and scikit-build, enabling C++ performance from Python code.