Announced at WWDC last week, SwiftUI is a new declarative UI framework that is described as an “innovative, exceptionally simple way to build user interfaces across all Apple platforms with the power of Swift.”. This approach to UI development has been popularised recently with emergence of Flutter, something that was also likely the inspiration for Jetpack Compose which was announced at Google I/O a few weeks back (SwiftUI appears to be at a significantly more advanced state of maturity than Jetpack Compose and is available to try out in Xcode 11 beta…but also important to note that it does require iOS 13).
Kotlin Multiplatform
Kotlin, unveiled by JetBrains in 2011, was initially a language targeted for the JVM. At I/O 2017, Google announced support for Kotlin for Android development and since then it’s popularity has exploded…galvanised further with the “Kotlin first” announcement at Google I/O 2019.
In recent times there’s also been an increasing interest in the Kotlin Multiplatform project and the opportunities it provides for having shared common Kotlin code running on multiple platforms…and, in particular, Kotlin/Native which allows the compilation of Kotlin for non-JVM platforms such as iOS. The feeling seems to be that this enables a more “palatable” level of code reuse (for example shared repository and data model) than say that espoused by “Cross Platform” frameworks where approach is to try and develop apps for multiple platforms from single code base (including UI). It’s worth noting that Kotlin has also become increasingly popular for developing back end services and I believe there’s also good opportunities in that context for sharing Kotlin code between client and server.
Galway Bus App
The GalwayBus project was created about 18 months ago, initially to allow exploring use of Kotlin and also the emerging Architecture Components for the development of Android applications. This became a platform then for trying out other technologies/libraries (e.g. migrating from Dagger to Koin and RxJava to Kotlin Coroutines)….and, in context of this discussion, having common Kotlin code running on Android and iOS. Kotlin Multiplatform is an area that’s still very much in development and I’d had somewhat limited success until about a week ago (just before WWDC as chance would have it!) when the stars aligned and the versions of various tools/libraries being used started working happily together!
#KotlinMultiplatform making some progress getting shared Kotlin code running on iOS and Android (in https://t.co/1docgdYeXR).Here it's using same Repo class written in Kotlin (though Android code is using suspend functions and iOS using methods that still call GlobalScope.launch) pic.twitter.com/nENNuVXmBX
— John O'Reilly (@joreilly) June 2, 2019
The shared Kotlin/Native code for this made use of:
- Kotlin Coroutines
- Ktor for REST API requests
- Kotlin serialization library
- CocoaPods integration for use in XCode project
And, then, SwiftUI emerges!
So, finally, we get to point of the post! A day after my tweet above I watched the announcement of SwiftUI at WWDC and thought that the combination of
this and shared Kotlin/Native code (for, at least in this case, shared repository and data model code) looked very interesting! I downloaded
XCode 11 beta and a couple of iterations later this is what I had…certainly a much simpler/cleaner way of creating a list UI like this (compared
to using likes of storyboards, UITableView
etc)
struct ContentView : View {
@EnvironmentObject var busRouteViewModel: BusRouteViewModel
var body: some View {
NavigationView {
List(busRouteViewModel.listRoutes.identified(by: \.timetableId)) { route in
RouteRow(route: route)
}
.navigationBarTitle(Text("Routes"), displayMode: .large)
.onAppear() {
self.busRouteViewModel.fetch()
}
}
}
}
struct RouteRow : View {
var route: BusRoute
var body: some View {
HStack {
Image("ic_bus")
VStack(alignment: .leading) {
Text(route.timetableId).font(.headline)
Text(route.longName).font(.subheadline)
}
}
}
}
The listRoutes
data was obtained using call, in Swift, to Kotlin GalwayBusRepository
code. Note how the Swift closure provided maps cleanly to Kotlin lambda.
class BusRouteViewModel: BindableObject {
var listRoutes: [BusRoute] = [] {
didSet {
didChange.send(self)
}
}
var didChange = PassthroughSubject<BusRouteViewModel, Never>()
let repository: GalwayBusRepository
init(repository: GalwayBusRepository) {
self.repository = repository
}
func fetch() {
repository.fetchBusRoutes(success: { data in
self.listRoutes = data
return KotlinUnit()
})
}
}
Following is excerpt from Kotlin GalwayBuyRepository
class. The suspend
version of fetchBusRoutes
is
used by Android code….while other one is called from iOS Swift code (use of Coroutines in Kotlin/Native code
is still something I’m getting my head around so probably cleaner way of doing this).
class GalwayBusRepository {
private val galwayBusApi = GalwayBusApi()
suspend fun fetchBusRoutes(): List<BusRoute> {
val busRoutes = galwayBusApi.fetchBusRoutes()
return transformBusRouteMapToList(busRoutes)
}
fun fetchBusRoutes(success: (List<BusRoute>) -> Unit) {
GlobalScope.launch(ApplicationDispatcher) {
success(fetchBusRoutes())
}
}
}
and following is the Kotlin BusRoute
data class used (instances of which we’re able to effectively access directly in Swift code)
@Serializable
data class BusRoute(
@SerialName("timetable_id")
val timetableId: String,
@SerialName("long_name")
val longName: String,
@SerialName("short_name")
val shortName: String)
It’s also interesting to note expect/actual use of ApplicationDispatcher
(used in call to launch()
above). This is defined in commonMain as
internal expect val ApplicationDispatcher: CoroutineDispatcher
In androidMain
internal actual val ApplicationDispatcher: CoroutineDispatcher
= Dispatchers.Default
and in iosMain as
internal actual val ApplicationDispatcher: CoroutineDispatcher
= NsQueueDispatcher(dispatch_get_main_queue())
internal class NsQueueDispatcher(private val dispatchQueue: dispatch_queue_t) : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) {
dispatch_async(dispatchQueue) {
block.run()
}
}
}
Though, as alluded to earlier, use of GlobalScope
above is far from ideal and means we don’t gain the main benefits of
structured concurrency that Kotlin Coroutines provide.
And, lastly, this is what it looks like in iOS Simulator (I clearly won’t be winning any design awards here :) but this should at least illustrate how easy it is, with SwiftUI, to add UI on top of shared Kotlin code )
Code for this is available in the GalwayBus github repo.
Featured in Kotlin Weekly Issue #150
Related tweet
SwiftUI meets Kotlin Multiplatform! https://t.co/AdJ22zizDZ
— John O'Reilly (@joreilly) June 9, 2019
Wrote a short post about experience of trying out #SwiftUI with #KotlinMultiplatform . Thanks @inyaki_mwc and @vinnycoyne for reviewing! https://t.co/QpB26eFiUZ