Integrating Shared Code into iOS: Direct, SwiftPM and CocoaPods

This content was generated with the assistance of AI and should be verified against the official Kotlin Multiplatform documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

A Kotlin Multiplatform module never runs inside an iOS app as Kotlin source. It is compiled by Kotlin/Native into an Apple framework — a binary plus an Objective-C header and a module map — and Xcode links and embeds that framework like any other. Everything on this page is about one question: who builds that framework, when, and how does Xcode find it? There are three answers for a local checkout (direct integration, a local Swift package, a local CocoaPods podspec) and two for a remote one (an XCFramework behind SwiftPM, or behind CocoaPods).

The baseline is the one used across this section: Kotlin 2.4.20, Compose Multiplatform 1.12.1, AGP 9.4, Xcode 26.4. Every example is a slice of TrailMate Everywhere, the multiplatform version of the Android section’s TrailMate app (package com.example.trailmate): the Gradle module sharedLogic produces a framework named Shared that the Xcode project iosApp imports. Stability levels used below: direct integration, the CocoaPods plugin and XCFramework export are Stable; SwiftPM import (the swiftPMDependencies \{} block, which also drives the CocoaPods migration) and Swift export are Alpha. Project layout is covered on Project Structure, Targets, Source Sets and the Hierarchy Template; what appears in Swift once the framework is imported on Objective-C Interop, Swift Export and SKIE. Xcode, signing and Swift packages themselves are not re-taught — see Xcode Projects and Previews and Swift Packages and Modularization.

The Options at a Glance

Two axes decide everything: local or remote (does the iOS app build the shared code from source, or consume a prebuilt binary?) and which dependency manager the iOS side uses.

Option Where the framework is built Needs Choose it when

Direct integration

By Gradle, from an Xcode Run Script build phase

A monorepo; no Pod dependencies in the Kotlin code

Default for new projects. The Kotlin Multiplatform IDE plugin and the web wizard generate it.

Local Swift package

By Gradle, from an Xcode scheme pre-action

A monorepo whose iOS code is split into local Swift packages

The iOS app is modularized with SwiftPM and a package, not the app target, imports Shared.

Local CocoaPods podspec

By Gradle, from script phases written into the generated podspec

A monorepo with a Podfile; Kotlin code that imports Pod libraries

The shared module must call Pods (for example an Obj-C-only SDK) through cinterop.

Remote XCFramework + SwiftPM

In CI, once per release; consumers download a ZIP

A Package.swift and a hosted ZIP

iOS developers must not need a JDK or Gradle, or the shared code has its own release cycle.

Remote XCFramework + CocoaPods

In CI, once per release

A published podspec

The consuming team is still CocoaPods-only.

Local options give instant feedback and one commit for a cross-cutting change. Remote options draw a hard line between the code bases: the iOS app sees the shared code as an ordinary third-party dependency.

Anatomy of the Framework

All options start from the same Gradle declaration: an Apple target with a framework binary. Without a framework binary, none of the integration tasks below is registered. This is the shared module’s configuration in TrailMate Everywhere, using the Android-KMP plugin that AGP 9 requires (never com.android.library, which can no longer be combined with the Kotlin Multiplatform plugin).

// sharedLogic/build.gradle.kts
plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidKotlinMultiplatformLibrary) // com.android.kotlin.multiplatform.library
}

kotlin {
    android {
        namespace = "com.example.trailmate.shared"
        compileSdk = 37
        minSdk = 26
    }

    listOf(
        iosArm64(),          // physical devices
        iosSimulatorArm64(), // Apple-silicon simulators
    ).forEach { target ->
        target.binaries.framework {
            baseName = "Shared"   // the name Swift will `import`
            isStatic = true       // see "Static or Dynamic?" below
            binaryOption("bundleId", "com.example.trailmate.shared")
        }
    }
}

baseName becomes the Swift module name, so it is part of your public API: renaming it breaks every import Shared. The bundleId binary option sets CFBundleIdentifier so that the framework is uniquely identified once embedded. The targets are the Tier 1 iOS targets; iosX64 (Intel simulators) is Tier 3 and is omitted unless a team still needs it.

Static or Dynamic?

isStatic = true isStatic = false (default)

Result

The Kotlin code is linked into the app executable.

A separate .framework bundle copied into the app’s Frameworks folder.

Embed and sign

Nothing to embed; the task still runs and is harmless.

Must be embedded and code-signed — the job of embedAndSignAppleFrameworkForXcode.

Whichever linking you choose, ship one Kotlin framework per app. Several independent Kotlin frameworks each carry their own copy of the runtime and cannot exchange Kotlin types; combine modules into an umbrella module instead: a module that declares the others with api(…​) and export(…​) in its framework block, as the official guide shows for XCFrameworks.

Direct Integration

Direct integration is the default and the simplest: an Xcode Run Script phase calls Gradle’s embedAndSignAppleFrameworkForXcode task, which builds the framework for the platform and architecture Xcode is building for, places it in Xcode’s derived-data build directory, and signs it. Xcode already exports the variables the task needs (CONFIGURATION, SDK_NAME, ARCHS, BUILT_PRODUCTS_DIR, FRAMEWORKS_FOLDER_PATH, EXPANDED_CODE_SIGN_IDENTITY), so the same script works for Debug and Release, device and simulator.

flowchart TD A["Build iosApp in Xcode
or an IDE iOS run configuration"] --> B{"OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED
= YES ?"} B -->|"yes: the IDE already ran Gradle"| F B -->|"no"| C["Run Script phase
./gradlew :sharedLogic:embedAndSignAppleFrameworkForXcode"] C --> D["Gradle compiles commonMain + iosMain
with Kotlin/Native
for CONFIGURATION, SDK_NAME, ARCHS"] D --> E["Shared.framework written to
derived data (BUILT_PRODUCTS_DIR)"] E --> F["Compile Sources
Swift sees Shared via framework search paths"] F --> G["Link Binary
static: Kotlin merged into the executable"] G --> H["Embed and sign
dynamic: copied into Frameworks and code-signed"] H --> I["iosApp.app"]

The diagram is the whole story of an Xcode build with direct integration. The Run Script phase must run before Compile Sources, because Swift compilation needs the framework’s header and module map.

Setting It Up in Xcode

Setting up the phase by hand takes five steps in the iosApp target’s settings; a project generated by the Kotlin Multiplatform IDE plugin already contains them.

  1. Add the phase. Build Phases, +, New Run Script Phase. Paste the script below.

  2. Disable "Based on dependency analysis" on that phase, so Xcode runs it on every build and does not warn about missing declared outputs.

  3. Move the phase above Compile Sources.

  4. Disable User Script Sandboxing (Build Settings, Build Options). A sandboxed script cannot launch the Gradle daemon or write the framework outside its allow-list, and the symptoms are cryptic (rsync errors, Operation not permitted).

  5. Stop a daemon that was started sandboxed with ./gradlew --stop, then build again.

# iosApp/iosApp.xcodeproj -- target iosApp, Build Phases, "Compile Kotlin Framework" (Run Script)
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
    echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED set to YES"
    exit 0
fi
cd "$SRCROOT/.."
./gradlew :sharedLogic:embedAndSignAppleFrameworkForXcode

The guard at the top is the contract between Xcode and the IDEs: when you start an iOS run configuration in IntelliJ IDEA or Android Studio, the IDE builds the Kotlin framework first (with full IDE support, such as breakpoints and incremental feedback) and sets OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED=YES, so Xcode does not build the same framework a second time. Run from Xcode alone, the variable is absent and the script builds it. $SRCROOT is the directory of iosApp.xcodeproj; TrailMate keeps iosApp one level below the Gradle root, hence $SRCROOT/...

Framework Search Paths and Build Type

The task writes the framework to $(BUILT_PRODUCTS_DIR), which Xcode already searches, so a hand-written project needs no extra FRAMEWORK_SEARCH_PATHS for the normal case. The project generated by the IDE plugin still sets the search path in an .xcconfig, which is also the right place for the two settings below.

// iosApp/Configuration/Config.xcconfig
// Where Xcode finds Shared.framework produced by the Run Script phase
FRAMEWORK_SEARCH_PATHS = $(inherited) $(SRCROOT)/../sharedLogic/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)

// Custom configurations ("Staging") must map to a Kotlin build type: Debug or Release
KOTLIN_FRAMEWORK_BUILD_TYPE = Debug

// The Kotlin framework is Objective-C based: keep module imports on
OTHER_LDFLAGS = $(inherited) -framework Shared

The Kotlin side only knows two build types. If you add Xcode configurations other than the default Debug and Release, define KOTLIN_FRAMEWORK_BUILD_TYPE as a user-defined build setting with the value Debug or Release; without it the task cannot decide which framework to build and fails. The -framework Shared linker flag matters for a static framework whose consumers do not import it through a module map; with the framework in the target’s Link Binary phase it is redundant.

Once the phase builds, shared Kotlin is an ordinary Swift module: import Shared, then call for example PlatformKt.platformName(), the Objective-C name of the top-level function from expect/actual and Calling Platform APIs. How Kotlin declarations are renamed is on Objective-C Interop, Swift Export and SKIE.

When Direct Integration Is Not Enough

Direct integration has one structural limit: the Kotlin module cannot depend on CocoaPods libraries, because the framework is built outside any Pod. If shared code must use an Obj-C-only SDK, either use a SwiftPM dependency through SwiftPM import (Alpha, below), a cinterop definition for a bundled .framework, or the CocoaPods approach.

Local SwiftPM Package

Modern iOS codebases are split into local Swift packages: a TrailMateKit package holds feature code, and the app target is a thin shell. A package cannot contain a Run Script phase, and SwiftPM’s own build (swift build) is not supported for the Kotlin framework, so the Kotlin build is moved to the scheme: a Build pre-action that runs the very same Gradle task before Xcode starts building. Available since Kotlin 2.0.0. The tutorial assumes the project already uses direct integration; if the framework currently comes from a binary target or a Pod, migrate first.

Set it up in Xcode with Product, Scheme, Edit Scheme, Build, Pre-actions, +, New Run Script Action, and select the app target under "Provide build settings from" so the script sees SDK_NAME and ARCHS.

# iosApp scheme "iosApp" -- Build, Pre-actions, New Run Script Action
cd "$SRCROOT/.."
./gradlew :sharedLogic:embedAndSignAppleFrameworkForXcode
// iosApp/Packages/TrailMateKit/Package.swift
// swift-tools-version:6.0
import PackageDescription

let package = Package(
    name: "TrailMateKit",
    platforms: [.iOS(.v16)],
    products: [
        .library(name: "TrailMateKit", targets: ["TrailMateKit"]),
    ],
    targets: [
        // No binaryTarget and no dependency on Shared: the framework comes from the
        // scheme pre-action and is found through the app target's build settings.
        .target(name: "TrailMateKit"),
        .testTarget(name: "TrailMateKitTests", dependencies: ["TrailMateKit"]),
    ]
)
// iosApp/Packages/TrailMateKit/Sources/TrailMateKit/SharedBridge.swift
import Shared

/// The only file in the package that touches Kotlin types; features call this wrapper.
public func trailMateGreeting() -> String {
    "TrailMate on \(PlatformKt.platformName())"
}

The trade-off against the Run Script phase is rebuild granularity: with the pre-action, a change in common code only needs the Kotlin side rebuilt to be reflected in Xcode, while the regular phase requires rebuilding both. Keeping every import Shared in a thin wrapper package, as above, also means that when a Swift export migration arrives (see Objective-C Interop, Swift Export and SKIE), only that package changes. For the Swift side of modularization, see Swift Packages and Modularization.

Local CocoaPods

The Kotlin CocoaPods plugin (org.jetbrains.kotlin.native.cocoapods, Stable) turns the Gradle module into a Pod: it generates a .podspec whose script phases invoke Gradle, and the iOS Podfile points at it with :path. Choose it only when the Kotlin code must import Pod libraries (pod("SDWebImage") produces cinterop bindings), or when the iOS project is already a CocoaPods one and must stay that way.

// sharedLogic/build.gradle.kts
plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidKotlinMultiplatformLibrary)
    alias(libs.plugins.kotlinCocoapods)   // org.jetbrains.kotlin.native.cocoapods
}

kotlin {
    iosArm64()
    iosSimulatorArm64()

    cocoapods {
        // Required
        version = "1.0.0"
        summary = "TrailMate shared logic"
        homepage = "https://example.com/trailmate"
        // Optional
        name = "SharedLogic"
        ios.deploymentTarget = "16.0"
        podfile = project.file("../iosApp/Podfile")   // lets Gradle run `pod install`

        framework {
            baseName = "Shared"       // required
            isStatic = true           // dynamic by default
            // export(project(":sharedUI")); transitiveExport = false (default)
        }

        // Custom Xcode configurations must map to a Kotlin build type
        xcodeConfigurationToNativeBuildType["Staging"] = NativeBuildType.DEBUG

        // A Pod used from Kotlin: cinterop bindings are generated
        pod("SDWebImage") {
            version = "5.20.0"
        }
    }
}
# iosApp/Podfile
target 'iosApp' do
  use_frameworks!
  platform :ios, '16.0'
  # The Kotlin module as a Pod: the folder holds the Gradle-generated podspec
  pod 'SharedLogic', :path => '../sharedLogic'
end

The workflow differs from the other options in four ways. The Gradle-generated podspec is produced by the podspec task. Open the .xcworkspace, never the .xcodeproj, once pod install has created it. The Podfile must contain use_frameworks! or use_modular_headers!, otherwise Pod dependencies cannot be imported into Kotlin/Native. And for a CI or fresh clone, run ./gradlew podInstall rather than a bare pod install: resource directories that Gradle creates at build time (for example Compose Multiplatform’s resources) do not exist yet, so a plain pod install configures Xcode without them and the app launches with missing fonts and images.

The cocoapods \{} block is the DSL entry point: version, summary and homepage are required podspec fields, framework \{} configures the produced binary (baseName is required), pod() declares a Pod used from Kotlin (linkOnly = true links a dynamic-framework Pod without cinterop bindings), podfile lets Gradle run pod install, specRepos \{} adds private spec repositories, and noPodspec() skips podspec generation.

Why CocoaPods and embedAndSign Cannot Be Combined

The documentation is explicit: the CocoaPods approach cannot be used together with the embedAndSignAppleFrameworkForXcode mechanism. The reason is that both own the same two jobs and produce conflicting results.

Concern Direct integration CocoaPods plugin

Who builds the framework

A Run Script phase in the app target

Script phases inside the Pod, run from Pods.xcodeproj

Where it lands

Derived data, found through the app’s framework search paths

The Pod’s own output directory, linked through the generated Pods project

Who embeds and signs

embedAndSignAppleFrameworkForXcode

CocoaPods' [CP] Embed Pods Frameworks phase

Source of truth for settings

The iosApp target

The Podfile and the podspec

Doing both would build the framework twice, embed two copies of the Kotlin runtime and fight over the same output files, so a project must pick one. Switching from CocoaPods to direct integration is therefore a migration, not a toggle: clean the build folder, run pod deintegrate in the Podfile’s directory, delete the cocoapods \{} block, the .podspec and the Podfile, then add the Run Script phase.

Remote Integration: an XCFramework Behind SwiftPM

When the iOS team should not need Gradle at all, build the framework once in CI and publish it as a binary. SwiftPM consumes such binaries as an XCFramework — a bundle holding one framework per platform slice, here device and simulator — described by a Package.swift with a binaryTarget. Fat or universal frameworks and bitcode are legacy: bitcode has been unsupported since Kotlin 2.0.20.

Building the XCFramework

// sharedLogic/build.gradle.kts
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework

kotlin {
    // Name of the module consumers import
    val xcframeworkName = "Shared"
    val xcf = XCFramework(xcframeworkName)

    listOf(
        iosArm64(),
        iosSimulatorArm64(),
    ).forEach { target ->
        target.binaries.framework {
            baseName = xcframeworkName
            // Uniquely identifies the framework (CFBundleIdentifier)
            binaryOption("bundleId", "com.example.trailmate.${xcframeworkName}")
            xcf.add(this)
            isStatic = true
        }
    }
}
# CI step (macOS runner), repository root of TrailMate Everywhere
./gradlew :sharedLogic:assembleSharedXCFramework
# Output: sharedLogic/build/XCFrameworks/release/Shared.xcframework

cd sharedLogic/build/XCFrameworks/release
zip -r Shared.xcframework.zip Shared.xcframework
swift package compute-checksum Shared.xcframework.zip

The task name is assemble<Name>XCFramework, built from the XCFramework(name) argument, so Shared gives assembleSharedXCFramework; add a build type for the debug variant (assembleSharedDebugXCFramework). The checksum is a SHA-256 of the ZIP that SwiftPM verifies on download: if the ZIP is re-uploaded under the same URL with different bytes, every consumer’s resolution fails, so never replace a published ZIP — publish a new version. Only the exported module’s declarations are visible to Swift; if the framework must expose several Gradle modules, assemble them into one umbrella module, as described next.

Hosting on GitHub Releases and Writing Package.swift

The ZIP needs a URL that downloads directly. A GitHub release is the common choice: create a release on a tag, attach the ZIP as an asset and copy its download link. Verify the link with curl before using it.

// trailmate-shared-spm/Package.swift   (a separate Git repository holding only the manifest)
// swift-tools-version:5.3
import PackageDescription

let package = Package(
    name: "Shared",
    platforms: [
        .iOS(.v16),
    ],
    products: [
        .library(name: "Shared", targets: ["Shared"]),
    ],
    targets: [
        .binaryTarget(
            name: "Shared",
            url: "https://github.com/example/trailmate-everywhere/releases/download/shared-1.2.0/Shared.xcframework.zip",
            checksum: "<output of swift package compute-checksum>"
        ),
    ]
)

Starting with Kotlin 2.4.20 (the docs name 2.4.20-RC3), when the project consumes SwiftPM dependencies the assemble…​XCFramework task generates a Package.swift and related SwiftPM files next to the XCFramework, so you can distribute those instead of writing the manifest by hand. For a project without SwiftPM dependencies the manifest above is still written manually. Validate either with swift package reset && swift package show-dependencies --format json, then push the manifest, create and push a semantic-version Git tag (SwiftPM versions packages by tags) and add it in Xcode with File, Add Package Dependencies.

Where to Keep Package.swift

Layout Advantage Drawback

Separate repository for the manifest (recommended)

The manifest is versioned independently of the Kotlin code; scales best.

Another repository, and CI must push the manifest and tag there.

Next to the Kotlin code

Simplest setup.

SwiftPM versions are Git tags and can collide with the project’s own release tags.

Inside the consumer app’s repository

No tag conflicts.

In a multi-package project only one consumer package can depend on the external module; CI must update the consumer repository on every release.

KMMBridge

KMMBridge from Touchlab automates exactly this pipeline: a Gradle plugin that assembles the XCFramework, uploads it (to GitHub Releases or other storage), computes the checksum, and updates the Package.swift or podspec in a consuming repository on each release, from a CI workflow. It is a third-party tool and is worth evaluating before hand-scripting the steps above. How the same release flow is wired into CI is on CI/CD for Kotlin Multiplatform with GitHub Actions.

Migrating from CocoaPods to SwiftPM

The documentation steers new projects toward SwiftPM, although CocoaPods is Stable and not deprecated. SwiftPM import (Kotlin code consuming Swift packages through swiftPMDependencies \{}) is Alpha, and the migration relies on it, so treat the result as a moving target and pin the Kotlin version. The migration has three phases, and the Kotlin Gradle plugin must be on the 2.4.20 line (the docs name its RC3 build).

Phase 1: Declare the SwiftPM Dependencies Next to the Pods

// sharedLogic/build.gradle.kts
kotlin {
    listOf(iosArm64(), iosSimulatorArm64()).forEach { target ->
        // 1c. cocoapods { framework { } } becomes binaries.framework { }
        target.binaries.framework {
            baseName = "Shared"
            isStatic = true
        }
    }

    // 1a. Declare the Swift package -- Alpha
    swiftPMDependencies {
        swiftPackage(
            url = url("https://github.com/firebase/firebase-ios-sdk.git"),
            version = from("12.5.0"),
            products = listOf(product("FirebaseAnalytics")),
        )
    }

    // The old declaration stays until the Xcode project is reconfigured
    cocoapods {
        pod("FirebaseAnalytics") {
            version = "12.5.0"
        }
    }
}

Sync Gradle to import the APIs of the Swift package. If the Pod and the package expose identical APIs, usually only the import lines in Kotlin change: import cocoapods.FirebaseAnalytics.FIRAnalytics becomes an import from the swiftPMImport package. The Firebase library is only an illustration of a Pod with a Swift-package twin; TrailMate itself has no Pod dependencies and would skip straight to direct integration.

Phase 2: Reconfigure the Xcode Project

The CocoaPods plugin owned the framework build, so the Xcode project must first adopt direct integration. The tooling generates the command for you: build the project in Xcode (the build fails on purpose) and read the error, or build from the terminal, then run the command it prints. It edits the .xcodeproj so that the build triggers the embed task, and it runs two Gradle tasks that give the page its two migration names.

# Run from iosApp/ -- the paths are printed in the build error and differ per machine
XCODEPROJ_PATH='/path/to/trailmate/iosApp/iosApp.xcodeproj' \
GRADLE_PROJECT_PATH=':sharedLogic' \
'/path/to/trailmate/gradlew' -p '/path/to/trailmate' \
    ':sharedLogic:integrateEmbedAndSign' ':sharedLogic:integrateLinkagePackage'
Task What it does

integrateEmbedAndSign

Modifies the .xcodeproj to run embedAndSignAppleFrameworkForXcode during the iOS build — that is, it installs the direct-integration Run Script phase for you.

integrateLinkagePackage

Adds the generated local Swift package that links the Kotlin framework and the SwiftPM dependencies declared in swiftPMDependencies \{} to the Xcode project.

After the command, resolve the SwiftPM dependencies in the IDE (Tools, Swift Package Manager, Resolve Dependencies, in IntelliJ IDEA) and build again.

Phase 3: Remove the CocoaPods Integration

# Run from iosApp/
pod deintegrate

If some Pods have no Swift package, keep them: edit the Podfile to delete only the line that mentions the Kotlin module (pod 'SharedLogic', :path ⇒ '../sharedLogic'), run pod install, and keep the dependency-free parts of the cocoapods \{} block. If everything moved to SwiftPM, delete the whole cocoapods \{} block, and the CocoaPods plugin alias from both the root and the module plugins \{} blocks. The documentation also offers a prepared AI-agent skill for this migration; review its diff.

For the Kotlin side of the new dependencies, and the klibs.io and cinterop alternatives, see Dependencies, klibs.io, cinterop and SwiftPM Import.

Monorepo or Separate iOS Repository?

The local-versus-remote decision is really a repository and team decision. The iOS app can live in the same repository as the shared module or consume it as a versioned binary.

Criterion Monorepo (local integration) Separate iOS repository (remote integration)

Change that spans shared code and UI

One commit, one pull request, instant in Xcode

Two pull requests and a release in between

iOS developers need a JDK and Gradle

Yes (the wrapper downloads Gradle; a JDK is required)

No: they download a ZIP

Release cadence

One cadence for all platforms

Shared code releases independently, with semantic versions

Mechanism

Direct integration, local package or local podspec

XCFramework with a Package.swift binary target, KMMBridge, or a Pod

Debugging shared code

Step from Swift into Kotlin in one project

Only with the Kotlin sources checked out; binary frameworks lose that

CI cost

One pipeline, builds Kotlin on every iOS build

Extra publishing pipeline; iOS builds are fast

Typical fit

One product team, TrailMate Everywhere itself

Platform teams, SDKs, or an iOS app adopted incrementally

For an existing iOS app adopting Kotlin, start with a local package in a branch (fast feedback), and move to a published XCFramework only when team boundaries require it; the adoption path is on Adopting Kotlin Multiplatform in Existing Apps.

Common Mistakes

Mistake Fix

The Run Script phase sits below Compile Sources

Move it above; Swift needs the framework header to compile.

Build fails with rsync or Operation not permitted

Disable User Script Sandboxing for the app target and run ./gradlew --stop so no sandboxed daemon survives.

embedAndSignAppleFrameworkForXcode task not found

The target has no binaries.framework declaration; add it.

Using both the cocoapods \{} block and the embed phase

Pick one: deintegrate the pods or remove the plugin.

Re-uploading a changed ZIP under the same release URL

Checksum failures for all consumers; publish a new version and tag.

Two Kotlin frameworks in one app

Duplicate runtimes and unshareable types; merge them into one umbrella module.