Swift and SwiftUI are Apple’s recommended technologies for app development, and with good reason. Their emphasis on safety, efficiency, performance, and expressiveness have made it easier than ever to build fast, polished, and robust apps for the Apple ecosystem.

Recent stories about Swift on Windows, Swift on the Playdate, and a SwiftUI-like library for Gnome highlight some of many the ongoing projects to expand Swift and SwiftUI’s reach beyond Apple devices. One effort that hasn’t received much publicity, however, is Swift for Android.

Android app development is typically done in Java or the more modern Kotlin language, both of which run on the Java Virtual Machine (JVM). Android app frameworks - from system utilities to the UI layer to third-party libraries - also run atop the JVM. But while many languages have been ported to the JVM, it is not an ideal fit for Swift. Among other issues, the JVM’s heap-based, garbage-collected environment can’t properly support Swift’s strict object lifetimes and deterministic performance characteristics.

A toolchain that compiles Swift to machine code for Android devices is the only way to truly bring Swift to Android. The Swift community has been working on such a toolchain for some time, and it’s making encouraging progress TODO: LINKS NEEDED. Once a supported toolchain is in place, the next step will be integration with the JVM, because print("Hello world!") isn’t enough. Real apps need access to Android’s JVM-based development frameworks.

The Skip team looks forward to Swift on Android, and we hope that we can even help the effort along. In the meantime, we’ve been working hard to bring one great aspect of Swift to Android and the JVM: the syntax!

Developers love Swift and SwiftUI’s concise but expressive syntax. We’re excited to share how Skip combines several Swift platform technologies to bring a large subset of this syntax to Android development, allowing you to target both Apple and Android devices with a single Swift and SwiftUI codebase.

Cross-platform development in Xcode

Overview

Skip works by transpiling your Swift source to Android’s native Kotlin, where it runs on the JVM. Transpiling allows you to see and even influence the output, and targeting the JVM maximizes interoperability: you can call Kotlin and Java APIs directly from your Swift code, with no bridging required. As explained in the introduction, however, running on the JVM also comes with important limitations and behavioral differences that you must always keep in mind. Your logic may be written in Swift, but when it runs on Android, it runs as JVM code.

This caveat applies to the UI layer as well. You write in SwiftUI, but Skip adapts this to Jetpack Compose, Android’s own modern UI framework. This gives Android users the native UI they’re used to, but it may not exhibit the exact behaviors you expect from SwiftUI.

Diagram of Skip's Swift-on-Android build process

Skip’s Swift-to-Kotlin transpilation integrates into your build process via a build plugin. The result is a workflow in which you work in Xcode, writing standard Swift and SwiftUI. Our build plugin leaves your source code untouched on Apple platforms, but generates, packages, and builds the equivalent Kotlin and Jetpack Compose code alongside it. One Swift and SwiftUI codebase, two fully native apps.

Let’s take a closer look at the Swift platform technologies that make this possible.

Transpilation with SwiftSyntax

SwiftSyntax is an open source Swift library by Apple that provides powerful tools for parsing and transforming Swift source code. SwiftSyntax has existed for some time, but it has only recently risen to prominence as the library powering Swift macros.

Our transpiler uses SwiftSyntax to parse your Swift code into a highly detailed syntax tree. Once we have this tree, we’re able to analyze it and translate it into an equivalent Kotlin tree. The fidelity that SwiftSyntax provides not only allows us to perfectly capture the semantics of the Swift source, but even to preserve your comments and formatting. The Kotlin we output is often indistinguishable from hand-written code.

Example Swift:

protocol Action {
    associatedtype R
    var name: String { get }
    func perform() throws -> R
}

/// Action to add two integers
struct AddAction: Action, Equatable {
    let lhs: Int // Left hand side
    let rhs: Int // Right hand side
    
    var name: String {
        return "Add"
    }

    func perform() -> Int {
        return lhs + rhs
    }
}
Transpiles to:

internal interface Action<R> {
    val name: String
    fun perform(): R
}

/// Action to add two integers
internal class AddAction: Action<Int> {
    internal val lhs: Int // Left hand side
    internal val rhs: Int // Right hand side

    override val name: String
        get() = "Add"

    override fun perform(): Int = lhs + rhs

    constructor(lhs: Int, rhs: Int) {
        this.lhs = lhs
        this.rhs = rhs
    }

    override fun equals(other: Any?): Boolean {
        if (other !is AddAction) return false
        return lhs == other.lhs && rhs == other.rhs
    }
}

ViewBuilders - a special case of Swift’s ResultBuilders - lie at the heart of SwiftUI’s easy-to-use syntax. SwiftSyntax is able to perfectly parse these as well, but this is one area where Skip’s output does not look hand-written. Kotlin doesn’t support the expressive ViewBuilder syntax, and Android’s Jetpack Compose UI framework is based on nested function calls instead. The transpilation from ViewBuilders to function calls is effective, but it results in mechanical-looking code.

You can see all of this in action using our online Swift-to-Kotlin transpiler playground. While it doesn’t replicate the integrated Xcode experience of the real thing, it is fun to experiment with, and it demonstrates the speed and sophistication of SwiftSyntax.

Swift Package Manager Integration

Translating Swift syntax into Kotlin is interesting, but a complete cross-platform solution must also integrate with your development workflow, support the Swift and SwiftUI APIs you’re accustomed to using, and scale to multi-module projects. For these needs, we leverage Swift Package Manager.

Swift Package Manager (SwiftPM) is the standard dependency management tool for Swift projects, and it has become an integral part of the Swift ecosystem. We use SwiftPM’s plugin support, dependency resolution, and module system.

Plugins

Swift Package Manager Plugins are a way to extend the functionality of SwiftPM. They allow developers to securely add custom commands or behaviors to the package manager. Parts of the plugin API are specifically designed for reading source code and generating additional source code, and we utilizes these capabilities to invoke our transpiler. Thanks to Xcode’s seamless SwiftPM integration, this happens transparently on every build, and any transpilation errors are surfaced right inline.

Dependency Resolution

We maintain a suite of open source libraries to mirror an ever-expanding subset of standard frameworks like Foundation, Observation, and SwiftUI for Android. SwiftPM allows you to easily integrate these libraries into your project, keep them up to date, and manage their transitive dependencies. Because SwiftPM’s Package.swift files have all the capabilities of Swift, we can add logic allowing you to exclude these Android libraries when performing Apple platform release builds, keeping your Apple releases free from any dependencies on Skip.

Modules

As the size of a project grows, so does the importance of modularization. SwiftPM makes it as easy as possible to break up your code into modules that you can test and iterate on independently. Compartmentalizing your codebase can also significantly improve compilation speeds, as modules that haven’t changed don’t need to be recompiled. We’re able to use this optimization as well, avoiding re-transpiling and recompiling for Android when a module hasn’t been modified.

Unit Testing

Unit testing is critical for verifying functionality and ensuring that what worked yesterday will still work tomorrow. This is doubly important for code that is translated into another language altogether!

XCTest has been Apple’s native framework for writing and running unit tests in Swift. Through our open source SkipUnit library, we support the XCTest API on top of JUnit, the venerable Java unit testing framework.

Swift Testing will replace XCTest starting with Xcode 16, and Skip will support Swift Testing in the coming weeks.

Diagram of Skip's XCTest-on-Android test process

Being able to run a unified set of unit tests across your Apple and Android targets is a critical aspect of sharing Swift source with Android. In fact the Skip modules themselves rely heavily on this testing support: we use GitHub actions to run our suite of Swift unit tests across both iOS and Android on every commit to prevent regressions.

Conclusion

Swift and SwiftUI are often associated with development for Apple devices, but their principles and paradigms are universal, and their use beyond Apple platforms is spreading. Work is ongoing to extend Swift support to Android, and we look forward to a supported Android toolchain with JVM integration. Meanwhile, advances in the Swift ecosystem have unlocked powerful integration possibilities. Skip leverages these advances to bring Swift and SwiftUI’s expressive syntax to Android development via transpilation to Kotlin and Jetpack Compose. While there are limitations and caveats to keep in mind when your code is translated to a different language and UI framework, this allows you to create cross-platform libraries and apps from a single Swift and SwiftUI codebase.