skip to content

How do you configure the iOS framework output from a KMP module using the framework DSL inside binaries.framework {} - name, static vs dynamic, baseName, and exporting dependencies?

level: middleimportance: should knowfreq 48%

answer

  1. binaries.framework { } per iOS target
  2. baseName = Swift import name
  3. isStatic = true -> static framework, no embed&sign
  4. export() re-exports API; needs api(...) dependency
  5. XCFramework() bundles device + simulator

basics

~20 s

Each iOS target has a binaries.framework {} block where you set the framework name, choose static or dynamic linking, and export other KMP libraries so their public API also shows up in the Swift-facing framework.

solid answer

~40 s

Inside each Kotlin/Native iOS target you call binaries.framework { }. Key knobs: baseName sets the framework/module name Swift imports; isStatic = true produces a statically-linked framework (avoids embed-and-sign); without it you get a dynamic framework. export(project(":someModule")) makes a dependency's public API visible in the generated Objective-C header so Swift can call it - by default only the current module's API is exposed, transitive APIs are hidden. Exported deps must be declared as api(...) in the source set. transitiveExport = true re-exports an exported dep's own exports. For multiple architectures you typically build an XCFramework via the XCFramework() helper so devices and simulators ship in one bundle.

code

kotlin · 16 lines
kotlin
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework

kotlin {
    val xcf = XCFramework("Shared")
    listOf(iosArm64(), iosSimulatorArm64()).forEach { target ->
        target.binaries.framework {
            baseName = "Shared"
            isStatic = true
            export(project(":analytics"))   // analytics must be api(...) in iosMain
            xcf.add(this)
        }
    }
    sourceSets {
        iosMain.dependencies { api(project(":analytics")) }
    }
}

go deeper

for a junior

Knows there's a framework block with a name but is fuzzy on static/dynamic and export.

for a middle

Configures baseName, isStatic, and export(), and knows exported deps need api(...).

for a senior

Explains header generation, Obj-C interop mapping, and when dynamic vs static is required; builds an XCFramework.

for a principal

Designs the public Swift API surface deliberately (@ObjCName, @HiddenFromObjC, exported module boundaries) and the distribution artifact strategy.

## The framework DSL Kotlin/Native targets expose a `binaries` container. For Apple targets you add a framework binary: ```kotlin kotlin { iosArm64 { binaries.framework { baseName = "Shared" // Swift: import Shared isStatic = true // static vs dynamic linking export(project(":analytics")) // re-export another module's API } } iosSimulatorArm64 { binaries.framework { baseName = "Shared"; isStatic = true } } } ``` ## What each knob does - **baseName** - the framework name and the Swift module name. `import Shared` in Swift maps to `baseName = "Shared"`. Defaults to the Gradle project name. - **isStatic** - `true` produces a **static framework** (object code linked into the app binary at app link time). `false` yields a **dynamic framework** embedded and signed at app build time. Static avoids the 'embed & sign' step; dynamic can be required when multiple frameworks must share one Kotlin runtime. - **export(...)** - by default the generated Objective-C header only exposes the **current module's** public declarations; transitive library APIs are hidden. `export()` re-exports a dependency's public API into the header. The exported dependency must be declared with `api(...)` (not `implementation(...)`) in the corresponding source set, otherwise the build fails. - **transitiveExport = true** - re-exports an exported dependency's own exported deps as well. - **linkerOpts / freeCompilerArgs** - pass linker and compiler flags. ## Headers and interop The framework ships an **Objective-C/Swift-compatible header**. Kotlin types are mapped: `suspend` functions become completion-handler or async Swift methods, sealed classes/enums map to Obj-C classes. You can tune naming with `@ObjCName` and hide declarations with `@HiddenFromObjC`. ## XCFramework for shipping A single `.framework` is per-architecture. To ship device + simulator in one artifact, use the `XCFramework` helper: ```kotlin val xcf = XCFramework("Shared") listOf(iosArm64(), iosSimulatorArm64()).forEach { it.binaries.framework { baseName = "Shared"; xcf.add(this) } } ``` Running the `assembleSharedXCFramework` task produces an XCFramework bundle.

  • Why must an exported dependency be declared with api(...) rather than implementation(...)?
    export() re-publishes the dependency's public API; api() keeps that API on the module's compile/publish surface, while implementation() hides it, so export() of an implementation dependency fails.
  • When would you choose isStatic = false (dynamic)?
    When several frameworks in the app must share a single Kotlin/Native runtime, or tooling requires a dynamically embedded framework; otherwise static is simpler and avoids embed-and-sign.

saying these in an interview costs you the question

  • Thinks export() works on an implementation(...) dependency
  • Believes baseName is cosmetic and unrelated to the Swift import
  • Confuses static vs dynamic framework consequences
  • Doesn't know transitive APIs are hidden by default in the Obj-C header

context