A teammate ships a single iosArm64 .framework and the app fails to run on the Apple-silicon simulator. Explain the architecture problem and how XCFramework plus the right targets fix it.
answer
- iosArm64 = device; iosSimulatorArm64 = Apple-silicon simulator
- device arm64 and simulator arm64 are different platforms
- lipo can't merge same-arch different-platform slices
- XCFramework = bundle of platform/arch slices, Xcode auto-picks
- assemble<Name>XCFramework task
basics
~20 siosArm64 is for real devices only. The simulator on an Apple-silicon Mac needs iosSimulatorArm64. A plain .framework holds one slice, so the simulator can't link it. Build both targets and bundle them in an XCFramework, which carries multiple slices.
solid answer
~40 siosArm64() produces an ARM64 device binary; the iOS Simulator on Apple-silicon Macs is also ARM64 but a different platform variant (simulator vs device), so the device framework won't link there. You need iosSimulatorArm64() (and iosX64() for Intel-Mac simulators). A single .framework only contains one platform/arch slice. The fix is an XCFramework, an Apple bundle that holds multiple framework slices keyed by platform+arch; Xcode auto-picks the matching slice at build time. In KMP, use the XCFramework("Name") helper, add each target's framework binary to it, and run the assemble<Name>XCFramework task. You can't lipo device+simulator arm64 together (same CPU, different platform); XCFramework is the supported solution. Ship it via SwiftPM binaryTarget, CocoaPods, or manual Xcode link.
code
kotlin · 12 linesimport org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
kotlin {
val xcf = XCFramework("Shared")
listOf(iosArm64(), iosSimulatorArm64(), iosX64()).forEach { target ->
target.binaries.framework {
baseName = "Shared"
xcf.add(this)
}
}
}
// ./gradlew assembleSharedXCFramework -> Shared.xcframework (device + simulator slices)go deeper
Recognizes the simulator needs a different target but may not know the exact name.
Names iosSimulatorArm64() and knows XCFramework bundles slices.
Explains the device-vs-simulator platform distinction, why lipo fails, and wires the XCFramework helper and assemble task.
Defines the org's target matrix and distribution pipeline (which simulator archs, CI agent types, release vs debug XCFramework, checksums).
## The root cause: device arm64 != simulator arm64 Apple-silicon simulators run **ARM64**, the same CPU family as iPhones. But a binary is tagged with a **platform**: `ios` (device) vs `ios-simulator`. A framework built only for `iosArm64()` is the **device** slice. When Xcode tries to build/run on the simulator it looks for an `ios-simulator` slice and finds none, so linking or running fails with a 'building for simulator but linking ... built for iOS' style error. So you must declare the simulator target: ```kotlin kotlin { iosArm64() // physical devices (ios, arm64) iosSimulatorArm64() // Apple-silicon simulator (ios-simulator, arm64) // iosX64() // Intel-Mac simulator (ios-simulator, x86_64) } ``` ## Why a plain .framework can't carry both A classic `.framework` Mach-O binary can hold multiple **CPU architectures** in one fat binary (`lipo`), but **not two slices that share the same CPU arch but differ in platform**. Device-arm64 and simulator-arm64 collide under `lipo`. So fat-merging is not a valid fix for device + Apple-silicon-simulator. ## XCFramework is the solution An **XCFramework** (`.xcframework`) is an Apple bundle that stores **multiple separate framework slices** indexed by `platform + arch + variant`. Xcode selects the correct slice automatically per build target. It cleanly supports device, simulator, macOS, etc., side by side. In KMP: ```kotlin import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework kotlin { val xcf = XCFramework("Shared") listOf(iosArm64(), iosSimulatorArm64(), iosX64()).forEach { t -> t.binaries.framework { baseName = "Shared" xcf.add(this) } } } ``` Run `./gradlew assembleSharedXCFramework` (or the debug/release variant tasks). The output `Shared.xcframework` contains a device slice and one or more simulator slices. ## Consuming the XCFramework - **SwiftPM:** `.binaryTarget(name: "Shared", path: "Shared.xcframework")` (or remote `url:checksum:`). - **CocoaPods:** reference via the generated podspec. - **Manual:** drag into Xcode 'Frameworks, Libraries, and Embedded Content'. ## Practical checklist - Always declare both `iosArm64()` and `iosSimulatorArm64()` (add `iosX64()` if Intel-Mac CI agents exist). - Package as XCFramework, never a single per-target framework for distribution. - Keep `baseName` identical across targets so Swift imports one module name.
- Why can't you lipo-merge the device and Apple-silicon simulator frameworks?Both are arm64, so lipo sees a duplicate architecture; they differ only by platform variant (ios vs ios-simulator), which lipo can't disambiguate. XCFramework keys on platform+arch.
- When do you still need iosX64()?When you must run on the iOS Simulator on Intel Macs (e.g., older CI agents), since that simulator is x86_64.
A plain framework is a single-key hotel room; an XCFramework is a key-card that opens whichever room (device or simulator) you arrive at.
saying these in an interview costs you the question
- Tries to fix it by lipo-merging device + simulator arm64
- Thinks the simulator can use the device framework because both are arm64
- Doesn't mention iosSimulatorArm64() at all
- Ships a single .framework for distribution instead of an XCFramework