Bridging the gap between Swift 5.5 concurrency and Kotlin Coroutines with KMP-NativeCoroutines

Share on:

I’ve written in previous posts about different approaches to consuming, from Swift code, suspend functions and Flows exposed from Kotlin Multiplatform shared code. Kotlin 1.4 added the capability to call suspend functions from Swift/Objective-C (with a number of caveats) and a variety of custom approaches have been used to map from Kotlin Flows to, for example, likes of Swift Combine publishers etc. Swift 5.5 added a number of interesting concurrency capabilities, in particular async/await and AsyncStream, that on the surface would seem to map quite nicely to Kotlin suspend functions and Flows. Initially these were limited to running on iOS 15 but with recent XCode 13.2 beta it’s now possible to use back to iOS 13 etc. In this post I’m going to outline exploration done to use the KMP-NativeCoroutines library to provide this mapping.

KMP-NativeCoroutines

KMP-NativeCoroutines is described as “a library to use Kotlin Coroutines from Swift code in KMP apps”. Along with new Swift 5.5 concurrency capabilities it also supports mapping to RxSwift and Combine and critically addresses 2 key areas

Updating BikeShare KMM sample

For the purposes of this exploration I made changes to the BikeShare KMM sample.

The first step is to add following to the plugins section of build.gradle.kts for common code.

id("com.rickclephas.kmp.nativecoroutines") version "0.7.0"

For this particular project we’re using CocoaPods so we updated Podfile as follows and then re-ran pod install. Note that the library now also supports use of Swift Packages.

target 'BikeShare' do
    pod 'common', :path => '../../common'
    pod 'KMPNativeCoroutinesAsync'
end


The following is excerpt from CityBikesRepository. We added use of @NativeCoroutineScope to allow us to specify the coroutine scope we want to use for requests from iOS code. The plugin will automatically generate fetchNetworkListNative and pollNetworkUpdatesNative wrappers respectively for the fetchNetworkList suspend function and pollNetworkUpdates function that returns a Flow.

@NativeCoroutineScope
private val coroutineScope: CoroutineScope = MainScope()


suspend fun fetchNetworkList(): List<Network> {
    return cityBikesApi.fetchNetworkList().networks
}

fun pollNetworkUpdates(network: String): Flow<List<Station>> = flow {
    while (true) {
        val stations = cityBikesApi.fetchBikeShareInfo(network).network.stations
        emit(stations)
        delay(POLL_INTERVAL)
    }
}


The following is our Swift View Model. We import KMPNativeCoroutinesAsync and can now make use of await asyncResult to invoke fetchNetworkListNative suspend function. For the pollNetworkUpdatesNative function that returns a Flow we can use asyncStream. Note also in this case that we store reference to associated Task and can use that in our SwiftUI code to trigger cancellation at appropriate time.

import Foundation
import common
import KMPNativeCoroutinesAsync

@MainActor
class CityBikesViewModel: ObservableObject {
    @Published var stationList = [Station]()
    @Published var networkList = [Network]()
    
    private var fetchStationsTask: Task<(), Never>? = nil
    
    private let repository: CityBikesRepository
    init(repository: CityBikesRepository) {
        self.repository = repository
    }
 
    func fetchNetworks() {
        Task {
            let result = await asyncResult(for: repository.fetchNetworkListNative())
            if case let .success(networkList) = result {
                self.networkList = networkList
            }
        }
    }
    
    
    func startObservingBikeShareInfo(network: String) {
        
        fetchStationsTask = Task {
            do {
                let stream = asyncStream(for: repository.pollNetworkUpdatesNative(network: network))
                for try await data in stream {
                    self.stationList = data
                }
            } catch {
                print("Failed with error: \(error)")
            }
        }
    }
    
    func stopObservingBikeShareInfo() {
        fetchStationsTask?.cancel()
    }
}


This is excerpt of SwiftUI code showing us starting/stopping observation of the bike network updates from onAppear and onDisapper.

struct StationListView: View {
    @ObservedObject var cityBikesViewModel : CityBikesViewModel
    var network: String
 
    var body: some View {
        List(cityBikesViewModel.stationList, id: \.id) { station in
            StationView(station: station)
        }
        .navigationBarTitle(Text("Bike Stations"))
        .onAppear {
            cityBikesViewModel.startObservingBikeShareInfo(network: network)
        }
        .onDisappear {
            cityBikesViewModel.stopObservingBikeShareInfo()
        }
    }
}


And finally this is screenshot from the iOS app.

BikeShare iOS screenshot


Featured in Kotlin Weekly Issue #275