Fix a shared Kotlin API the iOS team cannot use comfortably from Swift
This shared code compiles and Android is happy with it, but the iOS team reports two problems: from Swift they must pass every argument to search, and a switch over SearchResult still demands a default branch even though all cases are listed.
Constraint: the fix must stay inside the shared module. Do not ask the iOS team to write a Swift wrapper, and do not enable any alpha compiler feature.
// commonMain
sealed class SearchResult {
data class Hits(val items: List<String>) : SearchResult()
data object Empty : SearchResult()
data class Failed(val reason: String) : SearchResult()
}
class SearchApi {
fun search(query: String, page: Int = 0, pageSize: Int = 20): SearchResult = TODO()
}
Diagnose the cause and fix it.
Kotlin/Native exports via an Objective-C header, which has no default arguments and no exhaustive sums — so Swift demands every parameter and still wants a default. Add explicit overloads and expose a discriminator Swift can switch over.
- ✗Expecting Kotlin default arguments to survive into the Objective-C header
- ✗Assuming a
sealed classstays exhaustive for a Swiftswitch - ✗Reaching for
@JvmOverloads, which is a JVM annotation and does nothing on Kotlin/Native
- →What happens to a
suspendfunction when it crosses into the Objective-C header? - →How would you keep the ergonomic Kotlin API for Android and still export a Swift-friendly one?
What is happening
Kotlin/Native exports the shared module to iOS through a generated Objective-C header, and Swift reads that header. Objective-C cannot express two things in the declaration above:
- default arguments — there is one method and every parameter is mandatory;
- exhaustive sums — a
sealed classarrives as a set of ordinary classes, so the Swift compiler cannot prove aswitchis complete and demands adefault.
@JvmOverloads does not help: it is a JVM annotation and has no effect on Kotlin/Native.
The fix
// commonMain
enum class SearchOutcome { HITS, EMPTY, FAILED }
class SearchResult private constructor(
val outcome: SearchOutcome,
val items: List<String>,
val reason: String?,
) {
companion object {
fun hits(items: List<String>) = SearchResult(SearchOutcome.HITS, items, null)
fun empty() = SearchResult(SearchOutcome.EMPTY, emptyList(), null)
fun failed(reason: String) = SearchResult(SearchOutcome.FAILED, emptyList(), reason)
}
}
class SearchApi {
fun search(query: String): SearchResult = search(query, 0, 20)
fun search(query: String, page: Int): SearchResult = search(query, page, 20)
fun search(query: String, page: Int, pageSize: Int): SearchResult = TODO()
}
Explicit overloads replace the default arguments, and the enum gives Swift a discriminator its switch can be exhaustive over. Android loses nothing — the ergonomic Kotlin API can stay as a thin layer on top.