Kotlin Coroutines and Swift, revisited
Tags: kotlin, kotlinmultiplatform, swift
Published: 2021-06-04 20:50:38

Last year I wrote about a pattern for interop between [Kotlin coroutines and RxSwift](https://dev.to/touchlab/working-with-kotlin-coroutines-and-rxswift-24fa). I appreciate the attention it received, particularly where people have applied it to [other reactive frameworks](https://johnoreilly.dev/posts/kotlinmultiplatform-swift-combine_publisher-flow/), and even including a [code-generation plugin](https://github.com/FutureMind/koru) using the same ideas. I figure it's about time I talk about my own updated thinking on these patterns.

If you haven't read the previous article, I suggest going through that first for context.

## Kotlin updates

We'll stick to the same repository class as the original article, and walk through exposing it to iOS.

```kotlin
class ThingRepository {
    suspend fun getThing(succeed: Boolean): Thing {
        delay(100)
        if (succeed) {
            return Thing(0)
        } else {
            error("oh no!")
        }
    }

    fun getThingStream(
        count: Int,
        succeed: Boolean
    ): Flow<Thing> = flow {
        repeat(count) {
            delay(100)
            emit(Thing(it))
        }
        if (!succeed) error("oops!")
    }
}
```

In the previous article, I suggested the following pattern for wrapping suspend functions with callbacks that could run on iOS

```kotlin
class SuspendWrapper<T>(private val suspender: suspend () -> T) {
    init {
        freeze()
    }

    fun subscribe(
        scope: CoroutineScope,
        onSuccess: (item: T) -> Unit,
        onThrow: (error: Throwable) -> Unit
    ): Job = scope.launch {
        try {
            onSuccess(suspender().freeze())
        } catch (error: Throwable) {
            onThrow(error.freeze())
        }
    }.freeze()
}
```

After having played with these patterns more, a drawback to this emerged. This class expects a `CoroutineScope` to be supplied by the caller (which will be in Swift) at subscription time. This can be nice for flexibility if it might be called in different contexts, but in the vast majority of cases in practice this will live in some sort of class with its own scope, and it's much more pleasant to work with the scope from Kotlin than from Swift. So let's make the scope a constructor parameter instead.

```kotlin
class SuspendWrapper<T : Any>(
    private val scope: CoroutineScope,
    private val suspender: suspend () -> T
) {
    init {
        freeze()
    }

    fun subscribe(
        onSuccess: (item: T) -> Unit,
        onThrow: (error: Throwable) -> Unit
    ) = scope.launch {
        try {
            onSuccess(suspender().freeze())
        } catch (error: Throwable) {
            onThrow(error.freeze())
        }
    }.freeze()
}
```
A similar change can be made for `FlowWrapper`. Now we can manage that scope in the iOS repository class, at the Kotlin level.

```kotlin
class ThingRepositoryIos(private val repository: ThingRepository) {
    private val scope: CoroutineScope =
        CoroutineScope(SupervisorJob() + Dispatchers.Default)

    init {
        freeze()
    }

    fun getThingWrapper(succeed: Boolean) =
        SuspendWrapper(scope) { repository.getThing(succeed) }

    fun getThingStreamWrapper(count: Int, succeed: Boolean) =
        FlowWrapper(
            scope, 
            repository.getThingStream(count, succeed)
        )
}
``` 
Now we can drop the scope parameter from the RxSwift subscribe calls.

## Swift repository wrappers

The previous article didn't consider Swift-side architecture beyond the `createObservable()` and `createSingle()` functions. But in practice you aren't likely to want to call these inline at every call-site. You can add one more wrapper layer in Swift so that the rest of the Swift side of the codebase doesn't need to know about the Kotlin classes at all. For example:

```swift
class ThingRepositoryRxSwift {
    private let delegate: ThingRepositoryIos
    
    init() {
        self.delegate = ThingRepositoryIos(repository: ThingRepository())
    }
    
    func getThing(succeed: Bool) -> Single<Thing> {
        createSingle(suspendWrapper: delegate.getThingWrapper(succeed: succeed))
    }

    func getThingStream(count: Int32, succeed: Bool) -> Observable<Thing> {
        createObservable(flowWrapper: delegate.getThingStreamWrapper(count: count, succeed: succeed))
    }
}
```

Now the rest of the code sees only `ThingRepositoryRxSwift` and its RxSwift API and the Kotlin essentially becomes an implementation detail.

## Combine

The other thing I've done more thinking about since the original article is how this can apply to the Combine framework. Combine has a `Publisher` type which represents an observable stream with a particular type of event and error. 

```swift
func createPublisher<T>(
    flowWrapper: FlowWrapper<T>
) -> AnyPublisher<T, KotlinError> {
    return Deferred<Publishers.HandleEvents<PassthroughSubject<T, KotlinError>>> {
        let subject = PassthroughSubject<T, KotlinError>()
        let job = flowWrapper.subscribe { (item) in
            let _ = subject.send(item)
        } onComplete: {
            subject.send(completion: .finished)
        } onThrow: { (error) in
            subject.send(completion: .failure(KotlinError(error)))
        }
        return subject.handleEvents(receiveCancel: {
            job.cancel(cause: nil)
        })
    }.eraseToAnyPublisher()
}
```

This makes use of a `PassthroughSubject` to handle the heavy lifting, and simply forwards the `Flow` events from our `FlowWrapper` callbacks to it. It makes use of the same `KotlinError` error type as the previous article. Note the `eraseToAnyPublisher()` call at the end, which cleans up our return type to `AnyPublisher<T, KotlinError>` instead of `Deferred<Publishers.HandleEvents<PassthroughSubject<T, KotlinError>>>`. Combine's generic internals are weird but they give us this utility to hide them.

For single-event streams, Combine has a `Future` type. Unfortunately, there's no `eraseToAnyFuture()` helper that I could find, so we still end up typed as a multi-event `Publisher` instead.

```swift
func createFuture<T>(
    suspendWrapper: SuspendWrapper<T>
) -> AnyPublisher<T, KotlinError> {
    return Deferred<Publishers.HandleEvents<Future<T, KotlinError>>> {
        var job: Kotlinx_coroutines_coreJob? = nil
        return Future { promise in
            job = suspendWrapper.subscribe(
                onSuccess: { item in promise(.success(item)) },
                onThrow: { error in promise(.failure(KotlinError(error))) }
            )
        }.handleEvents(receiveCancel: {
            job?.cancel(cause: nil)
        })
    }
    .eraseToAnyPublisher()
}
```

You could also do something similar with a `PassthroughSubject`, but I suspect (without having profiled extensively) that this version is probably lighter. There's also [async/await support](https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md) on the horizon for Swift, which would be nice to integrate here in the future.

### SwiftUI

One of the nice things about using Combine is it integrates well with SwiftUI. You can create a model class like

```swift
class ThingModel: ObservableObject {
    @Published
    var thing: Thing = Thing(count: -1)
    
    var cancellables = Set<AnyCancellable>()
    
    init(_ repository: ThingRepositoryIos) {
        createPublisher(
            flowWrapper: repository.getThingStreamWrapper(
                count: 100, 
                succeed: true)
            )
            .replaceError(with: Thing(count: -1))
            .receive(on: DispatchQueue.main)
            .sink { thing in self.thing = thing }
            .store(in: &cancellables)
    }
}
```

and then observe the `Thing` in a view like 

```swift
struct ThingView : View {
    @ObservedObject
    var thingModel: ThingModel
    
    init(_ repository: ThingRepositoryIos) {
        thingModel = ThingModel(repository)
    }
    
    var body: some View {
        Text("Count: \(thingModel.thing.count)")
    }
}
```

## Isn't this a lot of boilerplate?

There's obviously a lot of layers here, and that can be a bit off-putting. However, I think this sort of layering is a helpful pattern in a lot of Swift/Kotlin interop.

We have two different languages that we're trying to make talk to each other. While for most simple use-cases the interop is [handled for us by the compiler](https://kotlinlang.org/docs/native-objc-interop.html), in more complex situations we need to carve out a shared cross-language interface by hand. This will often take the form of a Kotlin wrapper layer that's less idiomatic to Kotlin consumers but is possible for Swift to consume, and then an extra Swift layer to take those Kotlin wrappers and massage them into more idiomatic Swift code.

We shouldn't be surprised that this extra glue code is needed, given the differences in language and environment. Most of it is either things we only need to write once (like the `SuspendWrapper` class in Kotlin or the `createObservable()` or `createPublisher()` functions in Swift), or things that follow very consistent patterns that we might be able to codegen (like creating `ThingRepositoryIos` based on `ThingRepository` in Kotlin, or `ThingModel` in Swift). 

Sure, this extra infrastructure wouldn't be needed if we were writing a pure native iOS app in Swift. But we've gained the ability to share Kotlin code into Swift in a more idiomatic way. I tend to think the tradeoff is worth it.

## Final thoughts

I'd like to further iterate on all these patterns in some form of codegen library in the future, as compiler plugins start to mature. I'm hopeful for a multiplatform version of [Kotlin Symbol Processing](https://github.com/google/ksp/) to help with this, though this won't be possible before Kotlin 1.5.20 at the earliest due to missing extension points in the Kotlin compiler. Fingers crossed for something this summer I guess.

> **2023 Update**
> _As of 2023, Touchlab has introduced a new tool to help with flow and suspend interop, as well as sealed classes wrapped as enums. We are pleased to introduce you to SKIE, Swift/Kotlin Interface Extender. For more information and to sign up for a demo, please visit [skie.touchlab.co](skie.touchlab.co)._

If you'd like to play with all of this yourself, updated code samples are available at the following repo:

{% github touchlab/SwiftCoroutines no-readme %}

As before, this includes nullable and non-null versions of both single-event and multiple-event streams, and Swift unit tests to verify it all works correctly. In addition, there's now a SwiftUI display to demo the Combine code, as well as log-based demos of RxSwift and Combine bindings.

---

Hope you enjoyed this! I'd love to hear feedback, either in the comments or on [Slack](https://kotlinlang.slack.com/archives/D2VU3UHU0) or [Twitter](https://twitter.com/RussHWolf). If you'd like to go deeper, feel free to [reach out to Touchlab](https://touchlab.co/contact-us/). We're also [hiring](https://touchlab.co/careers-3/)!