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 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
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()
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
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.
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"