src-ios/ tree; Inside a Duet Android app walks the same repository’s Android side.
The worked example is Memory Lane, live on the App Store, 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.What does the tree look like?
The iPhone-relevant parts of Memory Lane’s tree: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:
src-ios/Libraries/<Project>Kit/Package.swift (as the scaffold emits it)
- 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? covers when that happens. - The bridge holds
BridgedStore, the one class every shell uses to hold a Kotlin store: a@Publishedmirror of the store’s state, a synchronoussend, and acancel()that stops the Kotlin runtime. Tutorial 2 writes it. - The shells. The scaffold emits each feature’s iOS side into this package as a
<Feature>Shelltarget: a Builder that makes the Kotlin store and the bridged mirror, aViewShellthat 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’sShells/instead; both layouts depend on the same bridge.
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.
RootBuilderbuilds the root level: aRootComponentthat owns the app-lifetime services (diagnostics, analytics, the inbound URL and push registry), adopts them as workers in aStoreHost, 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). - Mounts. The scaffold writes one
Features/<Feature>Mount.swiftper feature: a function that calls the feature’s Builder, wraps the view in aMountedFeature, and gives the scene the mount’sactivateanddeactivatebracket. A child’s delegate events are routed in that file’sonDelegateclosure. - The root screen.
RootScreenrenders 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.
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 byduet design-tokensfromparity/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 failsduet design-tokens --check.Libraries/ThemedControls/holds the shared controls built on the theme. The scaffold emitsAppButton, the one action-button style every screen uses, with primary, secondary and destructive roles. Memory Lane keeps its button,MLButton, in the Theming package.
What is in the app target?
src-ios/App/<Project>/ holds what only the app’s entry objects can own:
SceneDelegatebuilds the root withRootBuilder(dependency: SceneComponent()).build(), putsRootScreenin aUIHostingControllerinside the theme’s scope, callsactivate(), and callsteardown()when the scene disconnects. In Memory Lane it also forwards every opened URL to the app-services registry the root returned.SceneComponentis the root level’s Dependency: the objects only the scene can supply. As emitted it is empty.AppDelegatecarries process-level duties; in Memory Lane, the launch services and the push-notification device token.
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:
- Increment
BUILD_NUMBER, for builds other than Debug. - Assemble the Kotlin core:
scripts/assemble_kit.sh debugfor a Debug build andreleaseotherwise, so the build links the framework that contains the latest Kotlin edit.
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.

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.
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: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>Kitwith the binary target and<Project>Bridge;<Project>MainwithRootComposition.swift(the root’s Dependency, Component and Builder),RootScreen.swift, the analytics and ingress workers, and a test target;ThemingandThemedControls;- the app target with its three entry files and the scheme’s pre-actions.
<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
Why is the Swift code in packages instead of the app target?
Why is the Swift code in packages instead of the app target?
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.Where is the feature logic?
Where is the feature logic?
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 shows a feature module.Does the iPhone app run a garbage collector?
Does the iPhone app run a garbage collector?
Yes. The XCFramework links the Kotlin/Native runtime, including its collector, into the app executable. Kotlin/Native GC and Swift ARC measures its pauses and states the rules for Swift code that holds Kotlin objects.
Can I edit the .xcodeproj?
Can I edit the .xcodeproj?
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.What does an iPhone-only Duet app look like?
What does an iPhone-only Duet app look like?
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 compares the two flavors.Modaal scaffolds this tree for you — say you’re building a Production app for Android + iOS in the sentence on the home screen.
Sources and further reading
- Kotlin Multiplatform: build final native binaries:
binaries.frameworkandXCFramework, the Gradle side of the Kit package’s binary target - Swift Package Manager: binary targets: how a package declares a prebuilt XCFramework
- XcodeGen: the project spec format
xcodegen.ymluses - WidgetKit: the extension type Memory Lane’s widgets use
- The Duet framework on GitHub:
DuetShells(ViewShell,StoreHost) and the toolchain
Read next
Inside a Duet Android app
The same repository’s Android side: feature modules in
commonMain, a thin Compose app module, builders and workers.Kotlin/Native GC and Swift ARC
What the Kit package’s framework links into the app, the collector’s measured pauses, and who frees what.
Tutorial 2: One Behavior, Two Apps
The Kit package built by hand: the framework, the bridge, the first shell and the iPhone app.
Duet glossary
Shell, Builder, Component, mount, worker and the other terms on this page.