Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
c650482
Update Kotlin to 2.4.0-dev-7398
rickclephas Mar 21, 2026
af8ce5a
Remove deprecationsProvider from buildValueParametersCopy
rickclephas Mar 21, 2026
2be4716
Flows are now properly exported in Swift export
rickclephas Mar 21, 2026
4f2d29c
Add logic to track active jobs when testing Swift export
rickclephas Mar 21, 2026
5e5203a
Update Flow integration tests for AsyncSequences from Swift Export
rickclephas Mar 21, 2026
339abbc
Bump sample deployment targets for Swift Export 2.4.0
rickclephas Mar 21, 2026
016f5c1
Add asyncSequence(for:) compatibility function for Swift Export in sa…
rickclephas Mar 21, 2026
5a4d9fe
Add Combine and RxSwift source compatibility functions for Swift Export
rickclephas Mar 21, 2026
05730f1
Update SWIFT_EXPORT.md
rickclephas Mar 21, 2026
fbfcb50
Increase delay in tests
rickclephas Mar 21, 2026
1d93125
Update Kotlin to 2.4.0-dev-7970
rickclephas Mar 26, 2026
a109c8c
Use asyncSequence(for:) from KotlinCoroutineSupport
rickclephas Mar 26, 2026
b826d07
Flows with nullable elements are working now
rickclephas Mar 26, 2026
39397c2
Flow cancellation is working now
rickclephas Mar 26, 2026
7b3c9e2
Update Kotlin to 2.4.0-dev-8268
rickclephas Mar 28, 2026
12cc909
`Flow<Unit>` is now exported as `Flow<Any>` in Swift Export
rickclephas Mar 28, 2026
cb6b3b1
Explicitly set `customNativeHome` to `null` in `BaseNativeEnvironment…
rickclephas Mar 28, 2026
ee2fe54
`activeJobCount` isn't updated immediately upon cancellation with Swi…
rickclephas Mar 28, 2026
b700850
Use `StateFlow.value` property directly using Swift Export
rickclephas Mar 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .idea/kotlinc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions KMPNativeCoroutinesCombine/FuturePublisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ public func createPublisher<Output, Failure: Error, Unit>(
.eraseToAnyPublisher()
}

/// This function provides source compatibility during the migration to Swift export.
///
/// You should migrate away from this function once you have fully migrated to Swift export.
@available(*, deprecated, message: "Kotlin Coroutines are supported by Swift export")
public func createPublisher<Sequence: AsyncSequence>(
for asyncSequence: @escaping () async throws -> Sequence
) -> AnyPublisher<Sequence.Element, Error> {
return createFuture(for: asyncSequence)
.flatMap { createPublisher(for: $0) }
.eraseToAnyPublisher()
}

/// Creates an `AnyPublisher` for the provided `NativeSuspend` that returns a `NativeFlow`.
/// - Parameter nativeSuspend: The native suspend function to await.
/// - Returns: A publisher that publishes the collected values.
Expand Down
10 changes: 10 additions & 0 deletions KMPNativeCoroutinesCombine/Publisher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ public func createPublisher<Output, Failure: Error, Unit>(
.eraseToAnyPublisher()
}

/// This function provides source compatibility during the migration to Swift export.
///
/// You should migrate away from this function once you have fully migrated to Swift export.
@available(*, deprecated, message: "Kotlin Coroutines are supported by Swift export")
public func createPublisher<Sequence: AsyncSequence>(
for asyncSequence: Sequence
) -> AnyPublisher<Sequence.Element, Error> {
return createPublisher(for: nativeFlow(for: asyncSequence))
}

/// Creates an `AnyPublisher` for the provided `NativeFlow`.
/// - Parameter nativeFlow: The native flow to collect.
/// - Returns: A publisher that publishes the collected values.
Expand Down
24 changes: 24 additions & 0 deletions KMPNativeCoroutinesCore/NativeFlow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,27 @@ public typealias NativeFlow<Output, Failure: Error, Unit> = (
_ onComplete: @escaping NativeCallback<Failure?, Unit>,
_ onCancelled: @escaping NativeCallback<Failure, Unit>
) -> NativeCancellable<Unit>

@available(*, deprecated, message: "Internal API used for Swift export source compatibility")
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public func nativeFlow<Sequence: AsyncSequence>(for asyncSequence: Sequence) -> NativeFlow<Sequence.Element, Error, Void> {
{ onItem, onComplete, onCancelled in
let task = Task {
do {
for try await element in asyncSequence {
await withUnsafeContinuation { continuation in
onItem(element, continuation.resume, ())
}
}
onComplete(nil, ())
} catch {
if error is CancellationError {
onCancelled(error, ())
} else {
onComplete(error, ())
}
}
}
return { task.cancel() }
}
}
11 changes: 11 additions & 0 deletions KMPNativeCoroutinesRxSwift/Observable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ public func createObservable<Output, Failure: Error, Unit>(
return createObservableImpl(for: nativeFlow)
}

/// This function provides source compatibility during the migration to Swift export.
///
/// You should migrate away from this function once you have fully migrated to Swift export.
@available(*, deprecated, message: "Kotlin Coroutines are supported by Swift export")
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public func createObservable<Sequence: AsyncSequence>(
for asyncSequence: Sequence
) -> Observable<Sequence.Element> {
return createObservableImpl(for: nativeFlow(for: asyncSequence))
}

/// Creates an `Observable` for the provided `NativeFlow`.
/// - Parameter nativeFlow: The native flow to collect.
/// - Returns: An observable that publishes the collected values.
Expand Down
13 changes: 13 additions & 0 deletions KMPNativeCoroutinesRxSwift/SingleObservable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ public func createObservable<Output, Failure: Error, Unit>(
.flatMap { createObservable(for: $0) }
}

/// This function provides source compatibility during the migration to Swift export.
///
/// You should migrate away from this function once you have fully migrated to Swift export.
@available(*, deprecated, message: "Kotlin Coroutines are supported by Swift export")
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public func createObservable<Sequence: AsyncSequence>(
for asyncSequence: @escaping () async throws -> Sequence
) -> Observable<Sequence.Element> {
return createSingle(for: asyncSequence)
.asObservable()
.flatMap { createObservable(for: $0) }
}

/// Creates an `Observable` for the provided `NativeSuspend` that returns a `NativeFlow`.
/// - Parameter nativeSuspend: The native suspend function to await.
/// - Returns: An observable that publishes the collected values.
Expand Down
30 changes: 26 additions & 4 deletions SWIFT_EXPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,11 @@ These are the known limitations with Swift export and KMP-NativeCoroutines.

## 🚨 `NativeSuspend` and `NativeFlow` are unsupported

At the moment Swift export doesn't support functional return types yet.
At the moment Swift export has some issues with functional return types and generics.

Unfortunately KMP-NativeCoroutines heavily relies on functional return types, making it incompatible with Swift Export.
For now the plugin just clones your original functions and properties to prevent your Kotlin builds from failing.

**Temporary workaround:**
You should disable any relevant code in Swift if you would like to try Swift export.

# Enabling Swift export

To enable Swift export with KMP-NativeCoroutines you'll need to activate the Swift export compatibility mode:
Expand Down Expand Up @@ -64,3 +61,28 @@ For Combine and RxSwift there are helper functions available, e.g.:
- let future = createFuture(for: randomLettersGenerator.getRandomLetters(throwException: throwException))
+ let future = createFuture(for: { await self.randomLettersGenerator.getRandomLetters(throwException: throwException) })
```

## Flows

To use `Flow`s you'll need to add the following import:
```swift
import KotlinCoroutineSupport
```

After that you can use `Flow`s as `AsyncSequence`s without any changes:
```swift
let sequence = asyncSequence(for: clock.time)
```

> [!NOTE]
> Upon cancellation Swift export will throw a `CancellationError` instead of ending the iteration by returning `nil`.

> [!NOTE]
> It's recommended to keep using the `asyncSequence(for:)` function for now.
> However this function is a no-op and can eventually be replaces with a call to `asAsyncSequence()`.

For Combine and RxSwift there are helper functions available, e.g.:
```diff
- let publisher = createPublisher(for: clock.time)
+ let publisher = createPublisher(for: asyncSequence(for: clock.time))
```
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
kotlin = "2.4.0-dev-7068"
kotlin = "2.4.0-dev-8268"
kotlin-idea = "2.3.0-dev-9992"
kotlinx-coroutines = "1.10.1"
kotlinx-binary-compatibility-validator = "0.16.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public final class com/rickclephas/kmp/nativecoroutines/compiler/config/Suffixes
}

public final class com/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport : java/lang/Enum {
public static final field FLOW_SUPPORTED Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
public static final field NO_FUNC_RETURN_TYPES Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
public static final field NO_THROWS_SUSPEND_FUNC Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
public static final field SUSPEND_FUNC_SUPPORTED Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ public final class com/rickclephas/kmp/nativecoroutines/compiler/config/Suffixes
}

public final class com/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport : java/lang/Enum {
public static final field FLOW_SUPPORTED Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
public static final field NO_FUNC_RETURN_TYPES Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
public static final field NO_THROWS_SUSPEND_FUNC Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
public static final field SUSPEND_FUNC_SUPPORTED Lcom/rickclephas/kmp/nativecoroutines/compiler/config/SwiftExport;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ public enum class SwiftExport {
NO_FUNC_RETURN_TYPES,
NO_THROWS_SUSPEND_FUNC,
SUSPEND_FUNC_SUPPORTED,
FLOW_SUPPORTED,
}

public val SWIFT_EXPORT: ConfigOptionWithDefault<Set<SwiftExport>> =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,6 @@ internal fun FirExtension.buildValueParametersCopy(
.let(substitutor::substituteOrSelf)
.toFirResolvedTypeRef()

@OptIn(SymbolInternals::class)
deprecationsProvider = parameter.fir.deprecationsProvider

// TODO: support defaultValue once exported to ObjC

annotations.addAll(buildAnnotationsCopy(parameter.resolvedAnnotationsWithClassIds))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal fun FirCallableSignature.getNativeType(
): ConeKotlinType {
var nativeType = getRawType(type)
if (SwiftExport.NO_FUNC_RETURN_TYPES !in swiftExport) {
if (type is CallableSignature.Type.Flow) {
if (type is CallableSignature.Type.Flow && SwiftExport.FLOW_SUPPORTED !in swiftExport) {
val typeArgs = arrayOf<ConeTypeProjection>(getRawType(type.valueType))
nativeType = ClassIds.nativeFlow.constructClassLikeType(typeArgs, type.isNullable)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ internal fun GeneratorContext.buildNativeFunctionBody(
val coroutineScope = irTemporary(irCallCoroutineScope(originalFunction, function))
var expression = irCallOriginalFunction(originalFunction, function)
if (SwiftExport.NO_FUNC_RETURN_TYPES !in swiftExport) {
if (callableSignature.returnType is CallableSignature.Type.Flow) {
if (callableSignature.returnType is CallableSignature.Type.Flow && SwiftExport.FLOW_SUPPORTED !in swiftExport) {
expression = irCallAsNativeFlow(expression, coroutineScope)
}
if (callableSignature.isSuspend && SwiftExport.SUSPEND_FUNC_SUPPORTED !in swiftExport) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ internal fun GeneratorContext.buildNativePropertyGetterBody(
val coroutineScope = irTemporary(irCallCoroutineScope(originalGetter, function))
var expression = irCallOriginalPropertyGetter(originalGetter, function)
if (SwiftExport.NO_FUNC_RETURN_TYPES !in swiftExport) {
if (callableSignature.returnType is CallableSignature.Type.Flow) {
if (callableSignature.returnType is CallableSignature.Type.Flow && SwiftExport.FLOW_SUPPORTED !in swiftExport) {
expression = irCallAsNativeFlow(expression, coroutineScope)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,50 @@ public void testViewmodelscope() {
}
}

@Nested
@TestMetadata("src/testData/codegen/swift13")
@TestDataPath("$PROJECT_ROOT")
public class Swift13 {
private void run(String fileName) {
runTest("src/testData/codegen/swift13/" + fileName);
}

@Test
public void testAllFilesPresentInSwift13() {
KtTestUtil.assertAllTestsPresentByMetadataWithExcluded(this.getClass(), new File("src/testData/codegen/swift13"), Pattern.compile("^(.+)\\.kt$"), Pattern.compile("^(.+)\\.fir\\.kt$"), true);
}

@Test
@TestMetadata("annotations.kt")
public void testAnnotations() {
run("annotations.kt");
}

@Test
@TestMetadata("coroutinescope.kt")
public void testCoroutinescope() {
run("coroutinescope.kt");
}

@Test
@TestMetadata("functions.kt")
public void testFunctions() {
run("functions.kt");
}

@Test
@TestMetadata("properties.kt")
public void testProperties() {
run("properties.kt");
}

@Test
@TestMetadata("viewmodelscope.kt")
public void testViewmodelscope() {
run("viewmodelscope.kt");
}
}

@Nested
@TestMetadata("src/testData/codegen/swift3")
@TestDataPath("$PROJECT_ROOT")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ abstract class AbstractBaseDiagnosticsTest<R : ResultingArtifact.FrontendOutput<
// Using NativeFirstStageEnvironmentConfigurator causes issues with the incompatible.kt tests
private class BaseNativeEnvironmentConfigurator(
testServices: TestServices
): NativeEnvironmentConfigurator(testServices)
): NativeEnvironmentConfigurator(testServices, null)
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,10 @@ abstract class AbstractFirBaseCodegenTest(
KmpNativeCoroutinesDirectives.STATE_FLOW_SUFFIX with "Flow"
}
listOf<Long>(
0b001, // Kotlin 2.2.21
0b011, // Kotlin 2.3.0
0b101, // Kotlin 2.3.20
0b0001, // Kotlin 2.2.21
0b0011, // Kotlin 2.3.0
0b0101, // Kotlin 2.3.20
0b1101, // Kotlin 2.4.0
).forEach { version ->
forTestsMatching("swift$version/*") {
defaultDirectives {
Expand Down
Loading