Make Kotlin Multiplatform feel native in Swift
Swiftify enhances Kotlin Multiplatform's Swift interop by generating convenience overloads for default parameters and converting Kotlin Flow to native Swift AsyncStream.
Kotlin 2.0+ exports suspend functions as Swift async throws automatically. But two pain points remain:
// Kotlin - nice default parameters
suspend fun getNotes(limit: Int = 10, includeArchived: Boolean = false): List<Note>// Swift without Swiftify - must specify ALL parameters
let notes = try await repo.getNotes(limit: 10, includeArchived: false)
// Kotlin Flow - requires complex FlowCollector protocol
class MyCollector: Kotlinx_coroutines_coreFlowCollector { ... }
repo.watchNote(id: "1").collect(collector: MyCollector()) { _ in }// Swift with Swiftify - convenience overloads!
let notes = try await repo.getNotes() // uses defaults
let notes = try await repo.getNotes(limit: 5) // partial defaults
// Kotlin Flow → native AsyncStream
for await note in repo.watchNote(id: "1") {
print("Updated: \(note.title)")
}| Kotlin | Swift | What Swiftify Does |
|---|---|---|
suspend fun with defaults |
async throws |
Generates convenience overloads |
Flow<T> |
AsyncStream<T> |
Wraps with native Swift API |
StateFlow<T> |
AsyncStream<T> |
Adds *Stream property |
| Sealed classes | Swift enums | 🚧 Preview |
// settings.gradle.kts
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
}
}
// build.gradle.kts
plugins {
kotlin("multiplatform")
id("io.swiftify") version "0.1.0-SNAPSHOT"
}class NotesRepository {
@SwiftDefaults
suspend fun getNotes(limit: Int = 10): List<Note> { ... }
@SwiftFlow
fun watchNote(id: String): Flow<Note?> = flow { ... }
}./gradlew linkDebugFrameworkMacosArm64 # Swift code auto-generated!// Swift - clean and native!
let notes = try await repo.getNotes() // default limit
let five = try await repo.getNotes(limit: 5) // custom limit
for await note in repo.watchNote(id: "1") {
print("Note: \(note.title)")
}Swiftify auto-detects your framework name from the Kotlin Multiplatform configuration - no manual setup required.
Just apply the plugin and add annotations to your Kotlin code:
plugins {
kotlin("multiplatform")
id("io.swiftify") version "0.1.0-SNAPSHOT"
}
kotlin {
iosArm64().binaries.framework {
baseName = "MyKit" // <- Swiftify auto-detects this
}
}
// That's it! Add annotations to your code:
class MyRepository {
@SwiftDefaults
suspend fun getData(): Data { ... }
}If you want to customize how transformations work:
swiftify {
// Framework name is auto-detected - no need to set it!
sealedClasses {
transformToEnum(exhaustive = true)
}
defaultParameters {
generateOverloads(maxOverloads = 5)
}
flowTypes {
transformToAsyncStream()
}
}| Approach | How it Works |
|---|---|
| Annotations | Add @SwiftDefaults, @SwiftFlow to specific declarations |
| DSL Rules | Configure global behavior for all declarations |
| Mixed | Use both for fine-tuned control |
class UserRepository {
@SwiftDefaults // Generates convenience overloads for defaults
suspend fun fetchUser(id: String, includeProfile: Boolean = true): User
// No annotation - uses Kotlin/Native's default behavior
suspend fun internalFetch(): Data
}Generates convenience overloads for functions with default parameters.
Swift doesn't support default parameters from Kotlin/Objective-C interfaces. This annotation generates overloaded methods that call through with default values.
@SwiftDefaults
suspend fun getNotes(
limit: Int = 10,
includeArchived: Boolean = false
): List<Note>Generated Swift (convenience overloads):
extension NotesRepository {
// Overload 1: no parameters (uses all defaults)
public func getNotes() async throws -> [Note] {
return try await getNotes(limit: 10, includeArchived: false)
}
// Overload 2: just limit (uses default for includeArchived)
public func getNotes(limit: Int32) async throws -> [Note] {
return try await getNotes(limit: limit, includeArchived: false)
}
// Full signature already provided by Kotlin/Native
}Works with both suspend and regular functions.
Wraps Kotlin Flow with native Swift AsyncStream for clean for await syntax.
@SwiftFlow
fun watchNote(id: String): Flow<Note?>
@SwiftFlow
val connectionState: StateFlow<ConnectionState>Generated Swift:
extension NotesRepository {
public func watchNote(id: String) -> AsyncStream<Note> {
return AsyncStream { continuation in
let collector = SwiftifyFlowCollector<Note>(
onEmit: { value in continuation.yield(value) },
onComplete: { continuation.finish() },
onError: { _ in continuation.finish() }
)
self.watchNote(id: id).collect(collector: collector) { _ in }
}
}
}
// StateFlow properties get "Stream" suffix to avoid naming conflicts
public var connectionStateStream: AsyncStream<ConnectionState> { ... }Usage:
// Clean for-await loop instead of FlowCollector
for await note in repo.watchNote(id: "1") {
print("Note updated: \(note.title)")
}Transforms a sealed class to a Swift enum.
@SwiftEnum(name = "NetworkResult")
sealed class NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error(val message: String) : NetworkResult<Nothing>()
object Loading : NetworkResult<Nothing>()
}Generated Swift:
@frozen
public enum NetworkResult<T> {
case success(data: T)
case error(message: String)
case loading
}Note: Sealed class transformation is currently preview-only and not included in implementation builds to avoid type conflicts with Kotlin-exported classes.
| Kotlin | Swift | Notes |
|---|---|---|
String |
String |
|
Int |
Int32 |
Kotlin/Native exports as Int32 |
Long |
Int64 |
|
Double |
Double |
|
Float |
Float |
|
Boolean |
Bool |
|
List<T> |
[T] |
|
T? |
T? |
null → nil |
Unit |
Void |
Swift doesn't support default parameters from Objective-C/Kotlin interfaces. Use @SwiftDefaults to generate convenience overloads:
@SwiftDefaults
suspend fun getProducts(
page: Int = 1,
pageSize: Int = 20,
category: String? = null
): ProductPageGenerated Swift overloads:
extension ProductRepository {
// No params - uses all defaults
func getProducts() async throws -> ProductPage {
return try await getProducts(page: 1, pageSize: 20, category: nil)
}
// Just page
func getProducts(page: Int32) async throws -> ProductPage {
return try await getProducts(page: page, pageSize: 20, category: nil)
}
// Page + pageSize
func getProducts(page: Int32, pageSize: Int32) async throws -> ProductPage {
return try await getProducts(page: page, pageSize: pageSize, category: nil)
}
// Full signature provided by Kotlin/Native
}your-project/
├── src/commonMain/kotlin/
│ └── com/example/
│ └── UserRepository.kt # Your Kotlin code with annotations
├── build/generated/swiftify/
│ ├── Swiftify.swift # Generated Swift extensions
│ ├── SwiftifyRuntime.swift # Generated Swift helpers (FlowCollector)
│ └── YourFramework.apinotes # API notes for Xcode
└── build.gradle.kts
| Task | Description |
|---|---|
swiftifyGenerate |
Generate Swift code |
swiftifyPreview |
Preview without writing files |
# Generate Swift wrappers
./gradlew swiftifyGenerate
# Preview specific class
./gradlew swiftifyPreview --class=com.example.UserRepository// src/commonMain/kotlin/com/example/NotesRepository.kt
package com.example
import io.swiftify.annotations.SwiftDefaults
import io.swiftify.annotations.SwiftFlow
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
class NotesRepository {
private val _notes = MutableStateFlow<List<Note>>(emptyList())
// @SwiftDefaults with default parameters
// Generates: getNotes(), getNotes(limit:)
@SwiftDefaults
suspend fun getNotes(
limit: Int = 10,
includeArchived: Boolean = false
): List<Note> {
delay(100)
return _notes.value.take(limit)
}
// @SwiftDefaults with multiple defaults
// Generates: createNote(title:), createNote(title:, content:)
@SwiftDefaults
suspend fun createNote(
title: String,
content: String = "",
pinned: Boolean = false
): Note {
val note = Note(id = "note_1", title = title, content = content)
_notes.value = listOf(note) + _notes.value
return note
}
// @SwiftFlow - converts to AsyncStream
@SwiftFlow
fun watchNote(id: String): Flow<Note?> = flow {
while (true) {
emit(_notes.value.find { it.id == id })
delay(1000)
}
}
}
data class Note(
val id: String,
val title: String,
val content: String = ""
)import SampleKit
// With Swiftify - clean and native!
let repo = NotesRepository()
// Convenience overloads for default parameters
let notes = try await repo.getNotes() // uses defaults
let five = try await repo.getNotes(limit: 5) // partial defaults
// Create with defaults
let note = try await repo.createNote(title: "Hello")
let note2 = try await repo.createNote(title: "Hello", content: "World")
// Flow → AsyncStream with for-await
for await note in repo.watchNote(id: "1") {
print("Updated: \(note.title)")
}swiftify/
├── swiftify-annotations/ # @SwiftDefaults, @SwiftFlow, @SwiftEnum
├── swiftify-swift/ # Swift type specifications (SwiftType, specs)
├── swiftify-dsl/ # Gradle DSL (swiftify { ... })
├── swiftify-analyzer/ # Kotlin source analyzer
├── swiftify-generator/ # Swift code generator
├── swiftify-linker/ # Framework linker (embeds Swift into framework)
├── swiftify-gradle-plugin/ # Gradle plugin
└── sample/ # Demo project with macOS app
Note: Kotlin 2.0+ natively exports suspend functions as Swift async/await. No Kotlin runtime bridge is needed - Swift helpers are generated at build time.
- Kotlin 2.0+
- Gradle 8.0+
- Xcode 15+
- macOS 13+ / iOS 16+
The sample/ directory contains a demo project showcasing Swiftify's two main features with an interactive before/after comparison.
# Build and run macOS demo
./gradlew :sample:linkReleaseFrameworkMacosArm64
open sample/macApp/macApp.xcodeprojThe demo app shows a before/after comparison for each Swiftify feature:
| Feature | Before (Without Swiftify) | After (With Swiftify) |
|---|---|---|
| async/await | Must specify all parameters | Convenience overloads with defaults |
| AsyncStream | Complex FlowCollector protocol | Native for await syntax |
Each feature includes a "Try it live" button that executes the actual Swiftify-generated code.
sample/
├── src/commonMain/kotlin/com/example/
│ ├── NotesRepository.kt # Primary demo (getNotes, watchNote)
│ ├── UserRepository.kt # User management examples
│ ├── ProductRepository.kt # E-commerce examples
│ └── ChatRepository.kt # Real-time messaging
├── macApp/ # macOS SwiftUI demo app
│ └── macApp/
│ └── ContentView.swift # Before/after comparison UI
└── build/generated/swiftify/
├── Swiftify.swift # Generated Swift extensions
├── SwiftifyRuntime.swift # Generated Swift helpers (FlowCollector)
└── SampleKit.apinotes # API notes for Xcode
# 1. Build the Kotlin framework (Swift code auto-generated!)
./gradlew :sample:linkReleaseFrameworkMacosArm64
# 2. Open and run in Xcode
open sample/macApp/macApp.xcodeprojNote: Swiftify generates Swift extensions automatically when building the framework.
| Document | Description |
|---|---|
| Developer Guide | Comprehensive guide with examples and best practices |
| Cheatsheet | Quick reference for common patterns |
| Architecture | Internal design and module structure |
# Build everything
./gradlew build
# Run tests
./gradlew test
# Publish to local Maven
./gradlew publishToMavenLocal
# go to sample folder
cd sample
# Build sample framework (auto-generates Swift)
../gradlew linkReleaseFrameworkMacosArm64
# Open demo app in Xcode
open sample/macApp/macApp.xcodeprojApache 2.0