KMP Project with Tuist: KMP iOS Build System Construction

0. Introduction
My name is Choi Garamoi, and I am responsible for developing the Android app "KINTO Kantan Moushikomi."
This app uses KMP and shares some business logic with iOS app.
This time, I've summarized the results of using Tuist to enable smoother development of a KMP project.
1. Overview
In Android Studio, the KMP Application project is created using the New Project Wizard. Once it is created, the Monorepo will look like this:
KmpProject/
├── androidApp/ # Android専用モジュール(Kotlin/Android)
├── iosApp/ # iOS専用モジュール(Swift)
└── shared/ # 共通ビジネスロジック(Kotlin/Multiplatform)
Similar to Gradle for Android, a build environment suitable for modularization and team development is also important in iOS. This article will show you how to build one using Tuist.
2. Challenges in Xcode Project
Xcode manages the information required to build an app in *.xcodeproj, but there are some challenges.
- Merge conflicts occur frequently: When settings are changed, Xcode automatically updates the
project.pbxprojfile. This file is in an unstructured text format, which often leads to Git conflicts when multiple people edit it simultaneously. - Practically meaningless diffs are generated: Xcode's GUI operations can generate many diffs without any real changes, cluttering the history.
- Difficult to automate: Many settings are GUI dependent, making it difficult to automate builds using CI/CD or scripts.
- Difficult to review:
project.pbxprojis hard to read, making it difficult to review changes. - Scalability is limited: As the team size grows, managing multiple targets and build settings can become cumbersome.
The *.xcodeproj directory is like a combination of Gradle and .idea directories in an Android project, so it is not possible to separate local Xcode settings from iOS app build settings.
Google Trends also shows that there are more searches for "xcode conflict" than for "xcode dev," indicating that there are more conflicts during development.
3. What is Tuist?
It is a tool that can generate Xcode projects and workspaces in the Swift language and can also build in combination with Xcode terminal tools.
Main features are as follows:
- Modularization support
- Environment-independent builds (team development oriented)
- Automation support
- Swift Package Manager support
Other Xcode build tools include Swift Package Manager, XcodeGen, and Bazel.
- Swift Package Manager : It only provides the dependency management functionality of Gradle (the
dependenciesblock). - XcodeGen : Insufficient checking of tool settings makes it prone to human error.
- Bazel : It is complex to use because it is intended for large-scale projects and is overengineered for small and medium-sized projects.
4. Introducing Tuist
The following steps are based on Migrate an Xcode project and reflect the latest information:
4-1. Install Tuist
brew update
brew upgrade tuist
brew install tuist
For installation methods other than Homebrew, refer to the Manual.
4-2. Add Tuist Config files
Add three files (Manifest files): Tuist.swift, Project.swift, and Tuist/Package.swift.
KmpWithSwift/
├── Tuist.swift
├── Tuist/
│ └── Package.swift
├── Project.swift
├── androidApp/
│ └── ...
├── iosApp/
│ └── ...
└── shared/
└── ...
4-3. Add a Module (Target) to the Settings
Project.swift is the main config file, and you can use the sample from Migrate an Xcode project as is for the other files.
4-3-1. Tuist.swift
import ProjectDescription
let tuist = Tuist(project: .tuist())
4-3-2. Project.swift
Add target settings and build scripts for KMP common modules.
infoPlist: Whole screen setup.scripts: Command to build the KMP common module.
import ProjectDescription
let project = Project(
name: "KmpWithSwift",
targets: [
.target(
name: "App",
destinations: .iOS,
product: .app,
bundleId: "ktc.garamoi.choi.kmp.with.tuist.App",
infoPlist: .extendingDefault(
with: [
"UILaunchScreen": [
"UIColorName": "",
"UIImageName": "",
],
]
),
sources: ["iosApp/iosApp/**"],
resources: ["iosApp/iosApp/**"],
scripts: [
.pre(
script: """
cd "$SRCROOT"
./gradlew :shared:embedAndSignAppleFrameworkForXcode
""",
name: "Build KMP"
)
]
)
]
)
4-3-3. Tuist/Package.swift
// swift-tools-version: 6.0
import PackageDescription
#if TUIST
import struct ProjectDescription.PackageSettings
let packageSettings = PackageSettings(
productTypes: [:]
)
#endif
let package = Package(
name: "App",
dependencies: [
]
)
4-4. Delete the Old Xcode Project
- Delete
./iosApp/iosApp.xcodeproj. - Add
*.xcodeprojto./.gitignore.
4-5. Check
Check whether Tuist has been set up correctly.
- Verify that the Tuist Manifest file can be opened in Xcode.
# KmpWithSwiftディレクトリで tuist edit - Generate an Xcode project with Tuist.
# KmpWithSwiftディレクトリで tuist generate - Once Xcode is open, run the app.
- If the app starts normally, the process is complete.
5. Separate iOS Settings from Common Features
The common module ./shared/build.gradle.kts does not properly separate the scope of responsibility for the common business logic build settings and the iOS-specific XCFramework build.
kotlin {
// ...
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "shared"
isStatic = true
}
}
// ...
}
5-1. Response
If you separate the iOS XCFramework settings below from :shared and move them to ios, you can set them more effortlessly and enhance modularity.
it.binaries.framework {
baseName = "shared"
isStatic = true
}
5-2. Procedure
- Build XCFramework from the
./iosApp/sharedmodule. - Update the scripts in the
Apptarget (./iosApp/iosAppdirectory).
5-2-1. Add the :iosApp:shared Module
Add the ./iosApp/shared/build.gradle.kts file and add the module to ./settings.gradle.kts. No Android settings are required.
// ./iosApp/shared/build.gradle.kts
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "shared"
isStatic = true
export(projects.shared)
}
}
sourceSets {
commonMain.dependencies {
api(projects.shared)
}
}
}
// ./settings.gradle.kts
// ... 省略 ...
rootProject.name = "KmpProject"
include(
":androidApp",
":iosApp:shared",
":shared"
)
Delete the XCFramework configuration from ./shared/build.gradle.kts.
// ./shared/build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
}
kotlin {
// ... 省略 ...
iosX64()
iosArm64()
iosSimulatorArm64()
// ... 省略 ...
}
// ... 省略 ...
5-2-2. Tuist Target Update
Update the Gradle command in scripts of Project.swift.
Change the module in scripts from :shared to the added :iosApp:shared (./gradlew :shared:embedAndSignAppleFrameworkForXcode ➡️ ./gradlew :iosApp:shared:embedAndSignAppleFrameworkForXcode).
// ./Project.swift
import ProjectDescription
let project = Project(
name: "KmpWithSwift",
targets: [
.target(
name: "App",
destinations: .iOS,
product: .app,
bundleId: "ktc.garamoi.choi.kmp.with.tuist.App",
infoPlist: .extendingDefault(
with: [
"UILaunchScreen": [
"UIColorName": "",
"UIImageName": "",
],
]
),
sources: ["iosApp/iosApp/**"],
resources: ["iosApp/iosApp/**"],
scripts: [
.pre(
script: """
cd "$SRCROOT"
./gradlew :iosApp:shared:embedAndSignAppleFrameworkForXcode
""",
name: "Build KMP"
)
]
)
]
)
6. Multimodularization
As your app scales, modularizing features becomes essential.
But if each feature requires :iosApp:shared, the Tuist config becomes as follows:
// ./Project.swift
import ProjectDescription
let project = Project(
name: "KmpWithSwift",
targets: [
.target(
name: "App",
destinations: .iOS,
product: .app,
bundleId: "ktc.garamoi.choi.kmp.with.tuist.App",
infoPlist: .extendingDefault(
with: [
"UILaunchScreen": [
"UIColorName": "",
"UIImageName": "",
],
]
),
sources: ["iosApp/iosApp/**"],
resources: ["iosApp/iosApp/**"],
dependencies: [
.target("FeatureA"),
.target("FeatureB")
]
),
.target(
name: "FeatureA",
destinations: .iOS,
product: .framework,
bundleId: "ktc.garamoi.choi.kmp.with.tuist.FeatureA",
infoPlist: .default,
sources: ["iosApp/FeatureA/**"],
resources: ["iosApp/FeatureA/**"],
scripts: [
.pre(
script: """
cd "$SRCROOT"
./gradlew :iosApp:shared:embedAndSignAppleFrameworkForXcode
""",
name: "Build KMP"
)
]
),
.target(
name: "FeatureB",
destinations: .iOS,
product: .framework,
bundleId: "ktc.garamoi.choi.kmp.with.tuist.FeatureB",
infoPlist: .default,
sources: ["iosApp/FeatureB/**"],
resources: ["iosApp/FeatureB/**"],
scripts: [
.pre(
script: """
cd "$SRCROOT"
./gradlew :iosApp:shared:embedAndSignAppleFrameworkForXcode
""",
name: "Build KMP"
)
]
)
]
)
This configuration has the following two major issues:
- A common XCFramework will be built for the number of feature modules (
:IOSApp:shared). - Depending on your feature module target settings and build options, there is a risk that the app may use multiple versions of
:iosApp:shared.
To solve this problem, wrap :iosApp:shared with a Tuist target.
6-1. Add Wrapping Target
Add the KmpCore target so that feature modules share Xcode targets instead of directly using the Gradle :shared module.
The source code is in iosApp/shared/**, which is the same as :iosApp:shared, but uses KmpCore as a wrapping target to utilize the namespace generated by KMP and to encapsulate it. As a result, the only target that holds information about the KMP common code is KmpCore.
// ./Project.swift
import ProjectDescription
let project = Project(
name: "KmpWithSwift",
targets: [
.target(
name: "App",
destinations: .iOS,
product: .app,
bundleId: "ktc.garamoi.choi.kmp.with.tuist.App",
infoPlist: .extendingDefault(
with: [
"UILaunchScreen": [
"UIColorName": "",
"UIImageName": "",
],
]
),
sources: ["iosApp/iosApp/**"],
resources: ["iosApp/iosApp/**"],
dependencies: [
.target("FeatureA"),
.target("FeatureB")
]
),
.target(
name: "FeatureA",
destinations: .iOS,
product: .framework,
bundleId: "ktc.garamoi.choi.kmp.with.tuist.FeatureA",
infoPlist: .default,
sources: ["iosApp/FeatureA/**"],
resources: ["iosApp/FeatureA/**"],
dependencies: [.target(name: "KmpCore")]
),
.target(
name: "FeatureB",
destinations: .iOS,
product: .framework,
bundleId: "ktc.garamoi.choi.kmp.with.tuist.FeatureB",
infoPlist: .default,
sources: ["iosApp/FeatureB/**"],
resources: ["iosApp/FeatureB/**"],
dependencies: [.target(name: "KmpCore")]
),
.target(
name: "KmpCore",
destinations: .iOS,
product: .framework,
bundleId: "ktc.garamoi.choi.kmp.with.tuist.KmpCore",
infoPlist: .default,
sources: ["iosApp/shared/**"],
resources: ["iosApp/shared/**"],
scripts: [
.pre(
script: """
cd "$SRCROOT"
./gradlew :iosApp:shared:embedAndSignAppleFrameworkForXcode
""",
name: "Build KMP"
)
]
)
]
)
6-2. Expose shared via KmpCore
Simply the KmpCore target dependency on shared does not allow access from FeatureA and FeatureB to :shared code.
Additional configuration is required to allow FeatureA and FeatureB to access the KMP common code (Gradle's :shared module) via KmpCore.
First, add the settings configuration to the KmpCore target.
// ./Project.swift
import ProjectDescription
let project = Project(
name: "KmpWithSwift",
targets: [
// ... 省略 ...
.target(
name: "KmpCore",
destinations: .iOS,
product: .framework,
bundleId: "ktc.garamoi.choi.kmp.with.tuist.KmpCore",
infoPlist: .default,
sources: ["iosApp/shared/**"],
resources: ["iosApp/shared/**"],
scripts: [
.pre(
script: """
cd "$SRCROOT"
./gradlew :iosApp:shared:embedAndSignAppleFrameworkForXcode
""",
name: "Build KMP"
)
],
settings: .settings(base: [
"FRAMEWORK_SEARCH_PATHS": "iosApp/shared/build/xcode-frameworks/**",
"OTHER_LDFLAGS": "-framework shared"
])
)
]
)
Add the following Swift file so that the KmpCore target exposes the shared namespace in the KmpCore namespace.
// ./iosApp/shared/KmpCore.swift
@_exported import shared
6-3. Check
After building the project in Xcode, FeatureA and FeatureB can access :shared via import KmpCore.
For example, if the :shared module has a SomeModel(shared/src/commonMain/kotlin/ktc/garamoi/choi/kmp/with/tuist/SomeModel.kt) class, it can be accessed from FeatureA as follows.
import Foundation
import KmpCore
public class SomeFeatureAClass {
let model: SomeModel
// ...
}
If a compilation error occurs, the initial build may become unstable due to build order or caching issues. In that case, you can resolve the issue by performing a clean build or building multiple times.
7. Conclusion
- Xcode's
*.xcodeprojis not suitable for automation and team development. - I recommend using Tuist as an alternative to
*.xcodeprojfor Xcode projects. - By wrapping the KMP common module's XCFramework generation in an Xcode project target, feature modularization becomes easier.
8. References
- Kotlin Multiplatform : The official Kotlin project that provides various tools for cross-platform development using the Kotlin language.
- Gradle : A de-facto build tool for Android and Java projects.
- Tuist : Build tools for Xcode projects.
- Swift : An OOP language developed by Apple to replace Objective-C.
- Xcode : An IDE for Apple platforms.
- Xcode / Projects and workspaces
- Swift Package Manager : The official dependency management tool for the Swift language.
- XcodeGen : A tool to generate Xcode Project in YAML and JSON.
- Bazel : A build tool developed by Google. Targets large-scale Monorepo.
- Monorepo Explained : A system for managing multiple software in a single repository.
- Google Trends :
xcode conflict,xcode merge,xcode dev: The proportion of Xcode conflict searches is higher when compared to general Xcode development searches. - What is
project.pbxprojin Xcode - Project configuration / Projects / Project settings : Explanation of the
.ideadirectory in Android Studio and IntelliJ IDEA. - Migrate an Xcode project : Manual steps to turn an existing Xcode project into a Tuist project.
- Homebrew : A system package management tool for macOS.
- Install Tuist
- Xcode / Bundles and frameworks / Creating a static framework
- Swift logo : The official logos can be downloaded below.
- Kotlin logo
- Gradle logo
- Tuist logo
- KINTO Kantan Moushikomi : Android App
- KINTO Kantan Moushikomi : iOS App
- Choi Garamoi
関連記事 | Related Posts

KMP Project with Tuist: KMP iOS Build System Construction

既存のアプリへのKMPの適用:チームの経験と成果

Kotlin Multiplatform Mobile (KMM)を使ったモバイルアプリ開発

Applying KMP to an Existing App: Our Team’s Experience and Achievements

Kotlin Multiplatform Mobile(KMM)およびCompose Multiplatformを使用したモバイルアプリケーションの開発

Kotlin Multiplatform (KMP)でのテストのすべて
We are hiring!
【プロジェクトマネージャー(iOS/Android/Flutter)】モバイルアプリ開発G/東京
モバイルアプリ開発GについてKINTOテクノロジーズにおける、モバイルアプリ開発のスペシャリストが集まっているグループです。KINTOやmy routeなどのサービスを開発・運用しているグループと協調しながら、品質の高いモバイルアプリを開発し、サービスの発展に貢献することを目標としています。
【ソフトウェアエンジニア(リーダークラス)】共通サービス開発G/東京・大阪・福岡
共通サービス開発グループについてWebサービスやモバイルアプリの開発において、必要となる共通機能=会員プラットフォームや決済プラットフォームなどの企画・開発を手がけるグループです。KINTOの名前が付くサービスやKINTOに関わりのあるサービスを同一のユーザーアカウントに対して提供し、より良いユーザー体験を実現できるよう、様々な共通機能や顧客基盤を構築していくことを目的としています。
