The app:ios-kit module is a Kotlin Multiplatform framework that packages the shared code for iOS. It's distributed as a CocoaPods pod and consumed by the iOS app (app:iosApp). This module acts as a bridge between Kotlin/Native code and Swift/SwiftUI.
- Package Kotlin Multiplatform code for iOS consumption
- Generate CocoaPods framework specification
- Expose Kotlin APIs to Swift
- Handle iOS-specific initialization
- Provide iOS bindings for shared UI
app:ios-kit (iOS Framework - CocoaPods Pod)
├── src/
│ └── commonMain/
│ └── kotlin/
│ └── com/softartdev/notedelight/
│ └── IosApp.kt # iOS app initialization
├── build.gradle.kts # Framework configuration
├── iosComposePod.podspec # Generated podspec
└── shared.podspec # Alternative podspec
iOS application initialization helper:
object IosApp {
fun initialize() {
// Initialize Koin for iOS
startKoin {
modules(iosModule)
}
}
@Composable
fun ComposeApp() {
App() // Shared Compose UI
}
}This provides a clean entry point for Swift code.
Configured in build.gradle.kts:
kotlin {
cocoapods {
summary = "Shared library for the NoteDelight app"
homepage = "https://github.com/softartdev/NoteDelight"
version = "1.0"
ios.deploymentTarget = "14.0"
// SQLCipher dependency
pod("SQLCipher", libs.versions.iosSqlCipher.get(), linkOnly = true)
framework {
baseName = "iosComposePod"
isStatic = false
}
// Disable podspec generation on non-macOS platforms
if (!OperatingSystem.current().isMacOsX) noPodspec()
}
}The build generates iosComposePod.podspec:
Pod::Spec.new do |spec|
spec.name = 'iosComposePod'
spec.version = '1.0'
spec.homepage = 'https://github.com/softartdev/NoteDelight'
spec.source = { :http=> ''}
spec.authors = ''
spec.license = ''
spec.summary = 'Shared library for the NoteDelight app'
spec.vendored_frameworks = 'build/cocoapods/framework/iosComposePod.framework'
spec.libraries = 'c++'
spec.ios.deployment_target = '14.0'
spec.pod_target_xcconfig = {
'KOTLIN_PROJECT_PATH' => ':app:ios-kit',
'PRODUCT_MODULE_NAME' => 'iosComposePod',
}
spec.script_phases = [
{
:name => 'Build iosComposePod',
:execution_position => :before_compile,
:shell_path => '/bin/sh',
:script => <<-SCRIPT
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
exit 0
fi
set -ev
REPO_ROOT="$PODS_TARGET_SRCROOT"
"$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \
-Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \
-Pkotlin.native.cocoapods.archs="$ARCHS" \
-Pkotlin.native.cocoapods.configuration="$CONFIGURATION"
SCRIPT
}
]
spec.dependency 'SQLCipher', '~> 4.5.5'
endGenerate or update podspec:
# Generate podspec
./gradlew :app:ios-kit:podspec
# Or let CocoaPods generate it automatically during pod installThe build produces:
build/cocoapods/framework/
└── iosComposePod.framework/
├── iosComposePod # Binary
├── Headers/
│ └── iosComposePod.h # Objective-C header
├── Modules/
│ └── module.modulemap # Module map
└── Info.plist # Framework metadata
- ✅ iOS (arm64 - physical devices)
- ✅ iOS Simulator (arm64 - Apple Silicon Macs, x86_64 - Intel Macs)
kotlin {
iosArm64() // Physical iOS devices
iosSimulatorArm64() // iOS Simulator on Apple Silicon
// iosX64() // iOS Simulator on Intel (can be added if needed)
}Kotlin classes/functions are automatically exposed to Swift:
// Kotlin
class NoteManager {
fun createNote(title: String): Note {
// ...
}
}
// Accessible in Swift as:
// let manager = NoteManager()
// let note = manager.createNote(title: "My Note")- Kotlin
fun→ Swiftfunc - Kotlin
class→ Swiftclass(notstruct) - Kotlin
object→ Swift singleton class - Kotlin
suspend fun→ Swiftasyncfunction (or callback)
Kotlin nullability is preserved in Swift:
// Kotlin
fun getName(): String? { /* ... */ }
// Swift
func getName() -> String? { /* ... */ }core:domain- Domain layercore:data:db-sqldelight- Data layercore:presentation- ViewModelsui:shared- Shared Compose UI
compose.ui- Compose Multiplatformcompose.runtime- Compose runtimecompose.foundation- Foundation componentscompose.material3- Material 3
SQLCipher(~> 4.5.5) - Database encryption
# Build for all iOS targets
./gradlew :app:ios-kit:build
# Build for specific target
./gradlew :app:ios-kit:linkDebugFrameworkIosArm64
./gradlew :app:ios-kit:linkDebugFrameworkIosSimulatorArm64# Sync framework for use in Xcode
./gradlew :app:ios-kit:syncFramework \
-Pkotlin.native.cocoapods.platform=iphonesimulator \
-Pkotlin.native.cocoapods.archs=arm64This is usually called automatically by CocoaPods during Xcode build.
In app/iosApp/Podfile:
target 'iosApp' do
use_frameworks!
platform :ios, '14.0'
# Local pod from ios-kit module
pod 'iosComposePod', :path => '../ios-kit'
endcd app/iosApp
pod installThis creates iosApp.xcworkspace which includes the framework.
import iosComposePod
import SwiftUI
@main
struct NoteDelightApp: App {
init() {
// Initialize Kotlin
IosApp.shared.initialize()
}
var body: some Scene {
WindowGroup {
ComposeViewController()
}
}
}
// Wrap Compose UI in UIViewController
struct ComposeViewController: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
return ComposeControllerKt.ComposeController()
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
// Update if needed
}
}The framework includes Compose Multiplatform UI:
// Exposed to Swift/UIKit
fun ComposeController(): UIViewController {
return ComposeUIViewController {
App() // Full Compose UI
}
}Compose UI runs in a UIViewController:
- Renders using Skia graphics
- Handles touch events
- Integrates with iOS lifecycle
- Supports iOS navigation
- Uses new Kotlin/Native memory model
- Objects can be shared across threads
- Automatic reference counting
Uses Stately for thread-safe state management:
dependencies {
implementation(libs.stately.common)
implementation(libs.stately.isolate) // iOS-specific
implementation(libs.stately.iso.collections) // iOS-specific
}- Build framework:
./gradlew :app:ios-kit:build - Open
iosApp.xcworkspacein Xcode - Set breakpoints in Swift code
- Run app in simulator or device
Kotlin code can be debugged from Xcode with proper symbol mapping.
The iOS kit includes multiplatform Compose UI tests that extend CommonUiTests from the ui/test module:
Location: app/ios-kit/src/commonTest/kotlin/IosUiTests.kt
Test Coverage:
- CRUD operations
- Title editing after create/save
- Database prepopulation
- Encryption flow
- Password settings
- Locale switching
Running Tests:
# Requires iOS Simulator to be running
./gradlew :app:ios-kit:iosSimulatorArm64TestTest Configuration:
- Tests run on iOS Simulator (arm64)
- Uses Compose Multiplatform testing API
- Database is automatically cleaned up before each test
- Handles iOS-specific database file cleanup (WAL, journal files)
Database Management: Tests use improved database deletion that properly handles:
- Main database file (
notes.db) - Write-Ahead Logging files (
notes.db-wal) - Shared memory files (
notes.db-shm) - Journal files (
notes.db-journal)
This ensures clean test state and prevents test interference.
After changing Kotlin code:
# Rebuild framework
./gradlew :app:ios-kit:build
# Reinstall pod
cd app/iosApp
pod install
# Or use pod update
pod update iosComposePodXcode will automatically rebuild the framework during build.
# GitHub Actions
- name: Build iOS Framework
run: ./gradlew :app:ios-kit:linkReleaseFrameworkIosArm64
- name: Build iOS Simulator Framework
run: ./gradlew :app:ios-kit:linkReleaseFrameworkIosSimulatorArm64For distribution, create an XCFramework:
# Build for all architectures
./gradlew :app:ios-kit:linkReleaseFrameworkIosArm64
./gradlew :app:ios-kit:linkReleaseFrameworkIosSimulatorArm64
# Create XCFramework
xcodebuild -create-xcframework \
-framework build/bin/iosArm64/releaseFramework/iosComposePod.framework \
-framework build/bin/iosSimulatorArm64/releaseFramework/iosComposePod.framework \
-output iosComposePod.xcframeworkWhen working with this module:
- Swift compatibility: Keep APIs Swift-friendly
- Nullability: Use nullable types appropriately for Swift interop
- Naming: Use clear, descriptive names (they appear in Swift)
- Memory: Be aware of reference cycles
- Threads: Use Stately for thread-safe state
- Testing: Test on both simulator and device
- Podspec: Regenerate after dependency changes
- Documentation: Document public APIs (appears in Xcode)
- CocoaPods: Keep CocoaPods configuration up to date
- Versioning: Update version when making breaking changes
// Good: Swift-friendly API
class NoteManager {
fun createNote(title: String, text: String): Note
suspend fun saveNote(note: Note): Result<Unit>
}
// Avoid: Generic types, complex hierarchies
class Manager<T> { // Generics don't translate well
fun process(item: T): T
}// Use sealed classes for errors (maps to Swift enums)
sealed class Result<T> {
data class Success<T>(val value: T) : Result<T>()
data class Error<T>(val message: String) : Result<T>()
}// Suspend functions become async in Swift
suspend fun loadNotes(): List<Note> {
// ...
}
// Swift:
// Task {
// let notes = try await loadNotes()
// }- Clean CocoaPods cache:
pod cache clean --all - Deintegrate and reinstall:
pod deintegrate && pod install - Check Xcode path:
xcode-select -p
- Framework not found: Run
./gradlew :app:ios-kit:build - Symbol conflicts: Check for duplicate dependencies
- Architecture mismatch: Verify target architectures
- Missing initialization: Ensure
IosApp.initialize()is called - Thread issues: Use Stately for shared state
- Memory issues: Check for retain cycles
- Packages:
ui:shared,core:presentation,core:data,core:domain - Consumed by:
app:iosApp - Alternative: Direct framework embedding (without CocoaPods)