Table of Contents

  1. iOS Toolchain Configuration
  2. CMAKE_SYSTEM_NAME=iOS
  3. Simulator vs Device Builds
  4. Framework Creation
  5. Code Signing for iOS
  6. Bitcode Considerations
  7. XCTest Integration
  8. CocoaPods Alongside CMake
  9. Fat Library Workarounds
Back to CMake Mastery Series

iOS Development

June 4, 2026 Wasil Zafar 10 min read

Cross-compile C++ for iOS using CMake — toolchain configuration, simulator vs device builds, framework creation, code signing, XCTest integration, and building XCFrameworks.

iOS Toolchain Configuration

CMake 3.14+ has built-in support for iOS cross-compilation via CMAKE_SYSTEM_NAME=iOS. When this is set, CMake automatically configures the correct compiler, sysroot, and target architecture for iOS. The Xcode generator is the most natural choice as it produces native .xcodeproj files.

# Basic iOS build with Xcode generator
cmake -G Xcode \
    -DCMAKE_SYSTEM_NAME=iOS \
    -DCMAKE_OSX_DEPLOYMENT_TARGET=15.0 \
    -DCMAKE_OSX_ARCHITECTURES=arm64 \
    -S . -B build-ios

# Build for device (Release)
cmake --build build-ios --config Release -- -sdk iphoneos

# Build for simulator
cmake --build build-ios --config Release -- -sdk iphonesimulator
cmake_minimum_required(VERSION 3.21)

# Set system name BEFORE project() for proper detection
set(CMAKE_SYSTEM_NAME iOS)
set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0")

project(iOSLibrary LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# Create the shared library
add_library(mylib STATIC
    src/core.cpp
    src/networking.cpp
    src/crypto.cpp
)

target_include_directories(mylib PUBLIC
    $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>
)

# Link iOS system frameworks
target_link_libraries(mylib PUBLIC
    "-framework Foundation"
    "-framework Security"
)
Static vs Shared on iOS: iOS apps cannot load dynamic libraries at runtime (only system frameworks). Always build your C++ libraries as STATIC for iOS, or package them as FRAMEWORK (which are static frameworks on iOS, unlike macOS where they can be dynamic).

CMAKE_SYSTEM_NAME=iOS

Setting CMAKE_SYSTEM_NAME to iOS triggers CMake's cross-compilation mode specifically for iOS. This sets up the correct sysroot, compiler flags, and platform detection. It must be set before the project() command.

# ios-toolchain.cmake — Custom toolchain (alternative to inline settings)
set(CMAKE_SYSTEM_NAME iOS)
set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0" CACHE STRING "Minimum iOS version")

# Architecture: arm64 for device, x86_64 for simulator (Intel Mac)
# On Apple Silicon Macs, simulator also uses arm64
set(CMAKE_OSX_ARCHITECTURES "arm64" CACHE STRING "Target architecture")

# Enable ARC (Automatic Reference Counting) for Objective-C++ files
set(CMAKE_XCODE_ATTRIBUTE_CLANG_ENABLE_OBJC_ARC YES)

# Standard C++ settings
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Using the toolchain file
cmake -G Xcode \
    -DCMAKE_TOOLCHAIN_FILE=cmake/ios-toolchain.cmake \
    -S . -B build-ios

# Detect platform in CMakeLists.txt
# CMAKE_SYSTEM_NAME == "iOS" when cross-compiling for iOS

Simulator vs Device Builds

iOS simulator and device use different SDKs and potentially different architectures. The Xcode generator handles this with the -sdk flag at build time. For Ninja/Makefiles, you need separate build trees.

# Xcode generator — single configure, build for either
cmake -G Xcode -DCMAKE_SYSTEM_NAME=iOS -S . -B build-ios

# Device build (arm64, iphoneos SDK)
cmake --build build-ios --config Release -- -sdk iphoneos

# Simulator build (arm64 on Apple Silicon, x86_64 on Intel)
cmake --build build-ios --config Release -- -sdk iphonesimulator

# Ninja — separate build trees required
cmake -G Ninja \
    -DCMAKE_SYSTEM_NAME=iOS \
    -DCMAKE_OSX_SYSROOT=iphoneos \
    -DCMAKE_OSX_ARCHITECTURES=arm64 \
    -DCMAKE_BUILD_TYPE=Release \
    -S . -B build-device

cmake -G Ninja \
    -DCMAKE_SYSTEM_NAME=iOS \
    -DCMAKE_OSX_SYSROOT=iphonesimulator \
    -DCMAKE_OSX_ARCHITECTURES="arm64;x86_64" \
    -DCMAKE_BUILD_TYPE=Release \
    -S . -B build-simulator
iOS Gotcha — Simulator on Apple Silicon: On M1/M2/M3 Macs, the iOS simulator runs arm64 code natively. This means device and simulator libraries have the same architecture — you cannot merge them with lipo. Use XCFramework (see below) to package both variants correctly.

Framework Creation

iOS frameworks bundle headers, the compiled library, and metadata into a single distributable unit. CMake can create these directly with the FRAMEWORK target property. For distribution, wrap device and simulator variants into an XCFramework.

cmake_minimum_required(VERSION 3.21)
set(CMAKE_SYSTEM_NAME iOS)
set(CMAKE_OSX_DEPLOYMENT_TARGET "15.0")

project(MyFramework VERSION 1.0.0 LANGUAGES CXX)

add_library(MyFramework STATIC
    src/api.cpp
    src/internal.cpp
)

# Configure as Framework
set_target_properties(MyFramework PROPERTIES
    FRAMEWORK TRUE
    FRAMEWORK_VERSION A
    MACOSX_FRAMEWORK_IDENTIFIER "com.example.MyFramework"
    MACOSX_FRAMEWORK_BUNDLE_VERSION "${PROJECT_VERSION}"
    MACOSX_FRAMEWORK_SHORT_VERSION_STRING "${PROJECT_VERSION}"
    PUBLIC_HEADER "include/MyFramework/api.h;include/MyFramework/types.h"
    XCODE_ATTRIBUTE_INSTALL_PATH "@rpath"
    XCODE_ATTRIBUTE_SKIP_INSTALL "NO"
)

target_include_directories(MyFramework PUBLIC
    $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
)
# Build XCFramework from device + simulator frameworks
# Step 1: Build for device
cmake --build build-ios --config Release -- -sdk iphoneos
# Step 2: Build for simulator
cmake --build build-ios --config Release -- -sdk iphonesimulator

# Step 3: Create XCFramework
xcodebuild -create-xcframework \
    -framework build-ios/Release-iphoneos/MyFramework.framework \
    -framework build-ios/Release-iphonesimulator/MyFramework.framework \
    -output MyFramework.xcframework

Code Signing for iOS

All code running on iOS devices must be signed. CMake configures code signing through Xcode attributes. For CI/CD, you typically use automatic signing with a provisioning profile.

cmake_minimum_required(VERSION 3.21)
set(CMAKE_SYSTEM_NAME iOS)
project(SignedApp LANGUAGES CXX)

add_executable(MyApp MACOSX_BUNDLE src/main.cpp)

# Code signing configuration
set_target_properties(MyApp PROPERTIES
    MACOSX_BUNDLE TRUE
    MACOSX_BUNDLE_BUNDLE_IDENTIFIER "com.example.myapp"
    MACOSX_BUNDLE_BUNDLE_VERSION "${PROJECT_VERSION}"
    MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"

    # Xcode signing attributes
    XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "iPhone Developer"
    XCODE_ATTRIBUTE_DEVELOPMENT_TEAM "YOUR_TEAM_ID"
    XCODE_ATTRIBUTE_PROVISIONING_PROFILE_SPECIFIER ""
    XCODE_ATTRIBUTE_CODE_SIGN_STYLE "Automatic"

    # Target device family (1=iPhone, 2=iPad, 1,2=Universal)
    XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY "1,2"
)
# Build and sign in one step (Xcode generator)
cmake --build build-ios --config Release -- \
    CODE_SIGN_IDENTITY="iPhone Distribution" \
    DEVELOPMENT_TEAM="ABCDE12345"

# For CI: use manual signing with exported provisioning profile
cmake --build build-ios --config Release -- \
    CODE_SIGN_IDENTITY="iPhone Distribution: Company Name (TEAM_ID)" \
    PROVISIONING_PROFILE_SPECIFIER="MyApp_Distribution"

Bitcode Considerations

Apple deprecated Bitcode in Xcode 14 (2022) and removed the requirement entirely. However, understanding the history matters for maintaining older projects and third-party libraries that may still reference it.

# Bitcode is NO LONGER REQUIRED (Xcode 14+, iOS 16+)
# But for legacy compatibility with older Xcode versions:

if(CMAKE_SYSTEM_NAME STREQUAL "iOS")
    # Xcode 14+ — explicitly disable bitcode
    set_target_properties(mylib PROPERTIES
        XCODE_ATTRIBUTE_ENABLE_BITCODE "NO"
    )

    # For older Xcode (pre-14) that still expects bitcode:
    # set_target_properties(mylib PROPERTIES
    #     XCODE_ATTRIBUTE_ENABLE_BITCODE "YES"
    #     XCODE_ATTRIBUTE_BITCODE_GENERATION_MODE "bitcode"
    # )
    # target_compile_options(mylib PRIVATE -fembed-bitcode)
endif()
Post-Bitcode Era: Since Xcode 14, Apple no longer accepts Bitcode submissions. Set ENABLE_BITCODE=NO in all new projects. If you encounter build errors about Bitcode from third-party libraries, rebuild them without the -fembed-bitcode flag or update to a newer version.

XCTest Integration

While XCTest is primarily an Objective-C/Swift framework, you can test C++ code through XCTest by writing thin Objective-C++ wrappers. CMake can set up the test bundle structure that Xcode expects.

cmake_minimum_required(VERSION 3.21)
set(CMAKE_SYSTEM_NAME iOS)
project(TestableLib LANGUAGES CXX OBJCXX)

# Main library
add_library(mylib STATIC src/math.cpp src/strings.cpp)

# XCTest bundle
if(BUILD_TESTING)
    enable_testing()

    add_library(MyLibTests MODULE
        tests/MathTests.mm    # Objective-C++ test wrapper
        tests/StringTests.mm
    )

    set_target_properties(MyLibTests PROPERTIES
        BUNDLE TRUE
        BUNDLE_EXTENSION "xctest"
        XCODE_ATTRIBUTE_WRAPPER_EXTENSION "xctest"
        MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/tests/Info.plist"
    )

    target_link_libraries(MyLibTests PRIVATE
        mylib
        "-framework XCTest"
        "-framework Foundation"
    )

    # Register with CTest
    add_test(NAME MyLibTests
        COMMAND ${CMAKE_COMMAND} -E echo "Run via Xcode: xcodebuild test"
    )
endif()
// tests/MathTests.mm — Objective-C++ XCTest wrapper
#import <XCTest/XCTest.h>
#include "mylib/math.h"  // C++ header

@interface MathTests : XCTestCase
@end

@implementation MathTests

- (void)testAddition {
    // Call C++ function from Objective-C++ test
    XCTAssertEqual(mylib::add(2, 3), 5);
}

- (void)testMultiplication {
    XCTAssertEqual(mylib::multiply(4, 5), 20);
}

- (void)testDivisionByZero {
    XCTAssertThrows(mylib::divide(10, 0));
}

@end

CocoaPods Alongside CMake

Many iOS projects use CocoaPods for Swift/ObjC dependencies while using CMake for C++ libraries. The two systems can coexist by building the CMake library separately and integrating it as a vendored framework or static library in the Podspec.

# Workflow: Build CMake library, then integrate via CocoaPods

# 1. Build XCFramework with CMake
cmake -G Xcode -DCMAKE_SYSTEM_NAME=iOS -S . -B build-ios
cmake --build build-ios --config Release -- -sdk iphoneos
cmake --build build-ios --config Release -- -sdk iphonesimulator
xcodebuild -create-xcframework \
    -library build-ios/Release-iphoneos/libmylib.a \
    -headers include/ \
    -library build-ios/Release-iphonesimulator/libmylib.a \
    -headers include/ \
    -output MyLib.xcframework

# 2. Reference in Podspec
# MyLib.podspec references the pre-built XCFramework
# MyLib.podspec
Pod::Spec.new do |s|
  s.name         = "MyLib"
  s.version      = "1.0.0"
  s.summary      = "High-performance C++ library"
  s.homepage     = "https://github.com/example/mylib"
  s.license      = "MIT"
  s.author       = "Developer"
  s.source       = { :http => "https://releases.example.com/MyLib-1.0.0.zip" }
  s.platform     = :ios, "15.0"

  # Use pre-built XCFramework
  s.vendored_frameworks = "MyLib.xcframework"

  # Or use pre-built static library
  # s.vendored_libraries = "lib/libmylib.a"
  # s.preserve_paths = "include/**"
  # s.pod_target_xcconfig = {
  #   "HEADER_SEARCH_PATHS" => "$(PODS_TARGET_SRCROOT)/include"
  # }
end

Fat Library Workarounds

Before XCFramework existed (Xcode 11+), developers used fat/universal libraries containing both device and simulator slices. This approach has significant limitations on Apple Silicon but is still encountered in legacy codebases.

# Legacy approach: lipo to merge device + simulator (Intel Mac only)
# WARNING: Does NOT work on Apple Silicon (both are arm64!)
lipo -create \
    build-device/libmylib.a \
    build-simulator/libmylib.a \
    -output libmylib-universal.a

# Verify architectures
lipo -info libmylib-universal.a
# Architectures in the fat file: arm64 x86_64
Fat Libraries Are Obsolete: On Apple Silicon Macs, both device and simulator use arm64. You cannot merge them with lipo because it rejects duplicate architectures. Always use XCFramework instead — it handles the device/simulator distinction at the framework level, not the architecture level.
iOS Modern Best Practice
Complete XCFramework Build Script

The modern replacement for fat libraries. This script builds for all targets and produces a single distributable XCFramework:

#!/bin/bash
# build-xcframework.sh — Build XCFramework for iOS distribution
set -e

PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_DIR="$PROJECT_DIR/build-xcframework"
OUTPUT="$PROJECT_DIR/MyLib.xcframework"

rm -rf "$BUILD_DIR" "$OUTPUT"

# Configure once with Xcode generator
cmake -G Xcode \
    -DCMAKE_SYSTEM_NAME=iOS \
    -DCMAKE_OSX_DEPLOYMENT_TARGET=15.0 \
    -S "$PROJECT_DIR" -B "$BUILD_DIR"

# Build for device (arm64)
cmake --build "$BUILD_DIR" --config Release -- \
    -sdk iphoneos ARCHS=arm64

# Build for simulator (arm64 + x86_64)
cmake --build "$BUILD_DIR" --config Release -- \
    -sdk iphonesimulator ARCHS="arm64 x86_64"

# Create XCFramework
xcodebuild -create-xcframework \
    -library "$BUILD_DIR/Release-iphoneos/libmylib.a" \
    -headers "$PROJECT_DIR/include" \
    -library "$BUILD_DIR/Release-iphonesimulator/libmylib.a" \
    -headers "$PROJECT_DIR/include" \
    -output "$OUTPUT"

echo "XCFramework created: $OUTPUT"