> ## Documentation Index
> Fetch the complete documentation index at: https://docs.modaal.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# The Structure of a Kotlin Multiplatform iPhone App

> A Duet iPhone app is a SwiftUI app over a prebuilt Kotlin core: a Kit package that links it, a Main package that composes the app, and a thin app target.

A Duet-built iPhone app on the Kotlin Multiplatform flavor is a SwiftUI app over a prebuilt Kotlin core. The feature logic runs inside one XCFramework that Gradle assembles from the shared Kotlin modules. The Swift code around it is split into Swift packages: a **Kit** package that links the framework and adapts each Kotlin store for SwiftUI, a **Main** package that composes the app, two theming packages, and an app target that holds only what the operating system's entry objects must. **Duet**, Modaal's cross-platform parity framework for native iOS and Android apps, scaffolds this shape. This page walks the `src-ios/` tree; [Inside a Duet Android app](/articles/duet-android-app-anatomy) walks the same repository's Android side.

<Note>
  The worked example is **[Memory Lane](https://memorylaneapp.lovable.app/)**, live on the [App Store](https://apps.apple.com/us/app/memory-lane-private-journal/id6760589051), the same app the Android page walks. Its package names begin with the project's name; this page writes that prefix as `<Project>`, the placeholder the scaffold fills with your app's name. Counts come from Memory Lane's repository.
</Note>

## What does the tree look like?

The iPhone-relevant parts of Memory Lane's tree:

```text theme={null}
src-kmp/
└── apple-umbrella/             # builds the Kotlin core as <Project>Kit.xcframework
src-ios/
├── App/
│   ├── xcodegen.yml            # the Xcode project spec; the .xcodeproj is generated
│   ├── xcconfig/               # Debug, AdHoc and AppStore build settings
│   ├── <Project>/              # the app target: AppDelegate, SceneDelegate, SceneComponent
│   └── <Project>Widgets/       # a WidgetKit extension
├── Libraries/
│   ├── <Project>Kit/           # the binary target over the XCFramework, and the bridge
│   ├── <Project>Main/          # the composition root, shells, views, workers
│   ├── Theming/                # the theme and its design tokens
│   ├── WidgetShared/           # data the app and the widget extension share
│   └── MemoryLaneConnector/    # a generated backend client
└── Subtrees/
    └── Priming/PrimingNode/    # one feature's iOS shell as its own package
scripts/
└── assemble_kit.sh             # assembles the core; every iOS build runs it first
parity/fixtures/                # the recordings the Kotlin and Swift lanes replay
```

The code under `src-ios/` is Swift. The iPhone app contains no copy of a reducer: the feature logic is compiled once from `src-kmp/` and linked as the XCFramework. Memory Lane's `src-ios/` holds 250 Swift files outside generated directories, 208 of them in the Main package.

## What is in the Kit package?

`src-ios/Libraries/<Project>Kit/` is where Swift meets the Kotlin core. Its manifest declares the framework as a binary target at the path `assemble_kit.sh` publishes to, and a bridge target that every shell depends on:

```swift src-ios/Libraries/<Project>Kit/Package.swift (as the scaffold emits it) theme={null}
.binaryTarget(
  name: "<Project>Kit",
  path: "../../../src-kmp/apple-umbrella/build/XCFrameworks/app/<Project>Kit.xcframework"
),
.target(
  name: "<Project>Bridge",
  dependencies: [
    .target(name: "<Project>Kit"),
    .product(name: "DuetShells", package: "duet"),
  ],
  linkerSettings: [
    .linkedLibrary("c++"),
    .linkedFramework("Foundation"),
  ]
),
```

* **The binary target** is a build product of the project's own Kotlin sources. `build/` is not committed, so a fresh clone has no framework until the first assemble; [How do you build it?](#how-do-you-build-it) covers when that happens.
* **The bridge** holds `BridgedStore`, the one class every shell uses to hold a Kotlin store: a `@Published` mirror of the store's state, a synchronous `send`, and a `cancel()` that stops the Kotlin runtime. [Tutorial 2](/tutorials/duet-02-two-apps#write-the-consumer-package-and-the-store-mirror) writes it.
* **The shells.** The scaffold emits each feature's iOS side into this package as a `<Feature>Shell` target: a Builder that makes the Kotlin store and the bridged mirror, a `ViewShell` that turns intents into actions and state into view state, the SwiftUI view, and a spec target that tests the shell. Memory Lane keeps its 14 shell directories in the Main package's `Shells/` instead; both layouts depend on the same bridge.

The Kotlin/Native collector frees a Kotlin object that Swift holds, on a collection after the last Swift reference is released. [Kotlin/Native GC and Swift ARC](/articles/kotlin-native-gc-swift-arc) covers what that framework links into the app and who frees what.

## What is in the Main package?

`src-ios/Libraries/<Project>Main/` composes the app. It depends on the Kit package, the two theming packages and the Duet shells and services packages, and it holds:

* **The composition root.** `RootBuilder` builds the root level: a `RootComponent` that owns the app-lifetime services (diagnostics, analytics, the inbound URL and push registry), adopts them as workers in a `StoreHost`, and then mounts the features. Composition is a Dependency, a Component and a Builder per level, the same three parts the Android app uses ([Tutorial 3](/tutorials/duet-03-composing-features)).
* **Mounts.** The scaffold writes one `Features/<Feature>Mount.swift` per feature: a function that calls the feature's Builder, wraps the view in a `MountedFeature`, and gives the scene the mount's `activate` and `deactivate` bracket. A child's delegate events are routed in that file's `onDelegate` closure.
* **The root screen.** `RootScreen` renders the mounted features. As emitted it shows one feature full-screen, with a tab strip once there are two; the app's real navigation replaces it as the product grows.
* **Workers.** Everything that touches the world, implemented in Swift behind the ports the Kotlin reducers name as effects. Memory Lane's `Workers/` has 21 of them: repository workers over its backend, push notifications, media storage, image downscaling, video transcoding, widget sync.

The Android counterparts of these parts are the builders, `MainNavHost.kt` and `workers/` in the Android app module.

## Where do the theme and the controls live?

* **`Libraries/Theming/`** holds the app's theme. Its semantic colors, fonts and gradients are generated by `duet design-tokens` from `parity/design-tokens.yaml`, the file the Android theme is generated from too, so both apps read the same token names. A hand edit to a generated file fails `duet design-tokens --check`.
* **`Libraries/ThemedControls/`** holds the shared controls built on the theme. The scaffold emits `AppButton`, the one action-button style every screen uses, with primary, secondary and destructive roles. Memory Lane keeps its button, `MLButton`, in the Theming package.

[Tutorial 7](/tutorials/duet-07-theming) builds the tokens and the generated code.

## What is in the app target?

`src-ios/App/<Project>/` holds what only the app's entry objects can own:

* **`SceneDelegate`** builds the root with `RootBuilder(dependency: SceneComponent()).build()`, puts `RootScreen` in a `UIHostingController` inside the theme's scope, calls `activate()`, and calls `teardown()` when the scene disconnects. In Memory Lane it also forwards every opened URL to the app-services registry the root returned.
* **`SceneComponent`** is the root level's Dependency: the objects only the scene can supply. As emitted it is empty.
* **`AppDelegate`** carries process-level duties; in Memory Lane, the launch services and the push-notification device token.

The Xcode project is generated: `xcodegen.yml` is the source, and the `.xcodeproj` is a build product. The spec gives the app scheme two build pre-actions, which run before Xcode plans the build:

1. **Increment `BUILD_NUMBER`**, for builds other than Debug.
2. **Assemble the Kotlin core**: `scripts/assemble_kit.sh debug` for a Debug build and `release` otherwise, so the build links the framework that contains the latest Kotlin edit.

The scaffold emits Debug and Release configurations. Memory Lane builds with three, Debug, AdHoc and AppStore, each with its own file in `xcconfig/` over a shared `Base.xcconfig`, and adds a WidgetKit extension, `<Project>Widgets`, that reads what the app writes through the `WidgetShared` package's App Group storage.

<Frame caption="Memory Lane's registration screen on an iPhone simulator and a Pixel 8 emulator. One shared reducer owns the flow on both platforms; the iPhone app's Main package composes the SwiftUI screen, and only iOS offers Sign in with Apple.">
  <img src="https://mintcdn.com/modaal/S5bsKen-_yplB54_/images/memory-lane-iphone-android-pair.png?fit=max&auto=format&n=S5bsKen-_yplB54_&q=85&s=52a79ac45b024e4522766ad8958f1b32" alt="Side by side: Memory Lane's registration screen in SwiftUI on an iPhone — hand-drawn clouds, a portrait illustration, Continue with Apple and Continue with Google buttons — and in Jetpack Compose on a Pixel 8 emulator — the same serif tagline with a single Continue with Google button" style={{ width: "560px" }} width="1140" height="1200" data-path="images/memory-lane-iphone-android-pair.png" />
</Frame>

## How do you build it?

Open the generated project in Xcode, or let Modaal build and run it. Every build through the app scheme runs the assemble pre-action first. The first assemble on a machine downloads the Kotlin/Native toolchain and takes several minutes; later ones take seconds. A fresh clone needs one assemble before Xcode opens the project, because Swift package resolution reads the binary target before any pre-action runs:

```sh theme={null}
scripts/assemble_kit.sh debug
(cd src-ios/App && xcodegen generate --spec xcodegen.yml)
open src-ios/App/<Project>.xcodeproj
```

Build simulator targets with `ARCHS=arm64` on the `xcodebuild` command line: the framework ships arm64 slices only, and a generic simulator build also compiles x86\_64. A concrete simulator destination is arm64 already.

## What does day one look like?

On the day the scaffold runs, the same shape is there at minimal size:

* `<Project>Kit` with the binary target and `<Project>Bridge`;
* `<Project>Main` with `RootComposition.swift` (the root's Dependency, Component and Builder), `RootScreen.swift`, the analytics and ingress workers, and a test target;
* `Theming` and `ThemedControls`;
* the app target with its three entry files and the scheme's pre-actions.

Each feature the scaffold adds is one `<Feature>Shell` target in the Kit package, with its spec target, and one `Features/<Feature>Mount.swift` in the Main package. The Kotlin side of the same feature is a module under `src-kmp/subtrees/`, and its recordings are under `parity/fixtures/`.

## Common questions

<AccordionGroup>
  <Accordion title="Why is the Swift code in packages instead of the app target?" icon="box">
    A Swift package builds and tests without the app. `swift test` in the Kit package runs every shell's spec against the linked Kotlin core, and the Main package has its own test target for the composition root. The app target stays three files; the shells, the composition root and the workers are in packages with test targets.
  </Accordion>

  <Accordion title="Where is the feature logic?" icon="brain">
    In the Kotlin modules under `src-kmp/subtrees/`, compiled into the XCFramework. The Swift side sends actions, renders state and performs effects through workers; every decision a recording pins is made in a Kotlin reducer. [Inside a Duet Android app](/articles/duet-android-app-anatomy#where-does-feature-logic-live) shows a feature module.
  </Accordion>

  <Accordion title="Does the iPhone app run a garbage collector?" icon="memory">
    Yes. The XCFramework links the Kotlin/Native runtime, including its collector, into the app executable. [Kotlin/Native GC and Swift ARC](/articles/kotlin-native-gc-swift-arc) measures its pauses and states the rules for Swift code that holds Kotlin objects.
  </Accordion>

  <Accordion title="Can I edit the .xcodeproj?" icon="file-code">
    Edit `src-ios/App/xcodegen.yml` and regenerate. The project file is generated from it, so an edit made in Xcode's project editor is lost at the next `xcodegen generate`.
  </Accordion>

  <Accordion title="What does an iPhone-only Duet app look like?" icon="mobile">
    On the Swift flavor the feature logic is Swift: each feature is a Swift package under `src-ios/Subtrees/`, with no binary target and no Kotlin/Native runtime. [The Duet overview](/articles/duet#the-two-duet-templates) compares the two flavors.
  </Accordion>
</AccordionGroup>

<Note>
  [Modaal](https://modaal.dev) scaffolds this tree for you — say you're building a **Production app** for **Android + iOS** in the sentence on the home screen.
</Note>

## Sources and further reading

* [Kotlin Multiplatform: build final native binaries](https://kotlinlang.org/docs/multiplatform-build-native-binaries.html): `binaries.framework` and `XCFramework`, the Gradle side of the Kit package's binary target
* [Swift Package Manager: binary targets](https://developer.apple.com/documentation/xcode/distributing-binary-frameworks-as-swift-packages): how a package declares a prebuilt XCFramework
* [XcodeGen](https://github.com/yonaskolb/XcodeGen): the project spec format `xcodegen.yml` uses
* [WidgetKit](https://developer.apple.com/documentation/widgetkit): the extension type Memory Lane's widgets use
* [The Duet framework on GitHub](https://github.com/modaal-agent/duet): `DuetShells` (`ViewShell`, `StoreHost`) and the toolchain

## Read next

<CardGroup cols={2}>
  <Card title="Inside a Duet Android app" icon="folder-tree" href="/articles/duet-android-app-anatomy">
    The same repository's Android side: feature modules in `commonMain`, a thin Compose app module, builders and workers.
  </Card>

  <Card title="Kotlin/Native GC and Swift ARC" icon="memory" href="/articles/kotlin-native-gc-swift-arc">
    What the Kit package's framework links into the app, the collector's measured pauses, and who frees what.
  </Card>

  <Card title="Tutorial 2: One Behavior, Two Apps" icon="2" href="/tutorials/duet-02-two-apps">
    The Kit package built by hand: the framework, the bridge, the first shell and the iPhone app.
  </Card>

  <Card title="Duet glossary" icon="book" href="/articles/duet-glossary">
    Shell, Builder, Component, mount, worker and the other terms on this page.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.