Skip to content

swift

21 posts with the tag “swift”

Skip 2025 Retrospective and 2026 Roadmap

As 2025 comes to a close, we’ve been reflecting on how far Skip has advanced this year. What began nearly three years ago with a simple desire to enable cross-platform app development with Swift and SwiftUI has grown into a thriving ecosystem, a strong community of developers and contributors, and a platform powering real production apps across iOS and Android.

This year wasn’t just about growth in numbers. It was about expanding depth and breadth: deeper integrations, stronger foundations, and a clearer vision for the future of native Swift across the dominant mobile platforms.

Native Swift on Android Becomes Officially Supported

Section titled “Native Swift on Android Becomes Officially Supported”

The highlight of 2025 by far was the official release of the Swift SDK for Android on swift.org, along with Skip’s support in the form of Skip Fuse. Prior to the advent of Skip Fuse, Skip operated solely in transpiled mode (now called “Skip Lite”), which converts Swift source code to Kotlin. Skip Fuse, on the other hand, builds natively-compiled Swift targeting the Android platform, which both eliminates the limitations imposed by source transpilation, as well as unlocks the universe of thousands of native Swift packages that are compatible with the Android platform.

Interest in the Swift SDK for Android has exploded since the initial announcement and follow-up blog posts on swift.org. We at Skip are proud to be founding members of the Swift Android workgroup, and we are committed to the platform’s enduring stability and support. And where the scope of the workgroup ends, we complete the picture by providing the tooling, libraries, and support needed to build universal apps from a single Swift codebase.

Liquid Glass and the Wisdom of Staying Native

Section titled “Liquid Glass and the Wisdom of Staying Native”

The launch of iOS 26 and the emergence of Liquid Glass as the new interface style was a pivotal moment for the cross-platform app development technosphere, as well as a powerful validation of Skip’s core philosophy. From day one, Skip has avoided intermediating or re-implementing SwiftUI on iOS or other Apple platforms. By staying fully native, Skip was able to support Liquid Glass on Day 1, automatically benefiting every Skip-based app without rewrites or workarounds (see our blog post on the topic).

In contrast, other cross-platform toolkits — such as Flutter and Compose Multiplatform — have found themselves stranded, incapable of adopting Liquid Glass and stuck on the previous UI generation with their mimicked faux-native components. For iOS users, that means outdated interfaces and an exacerbation of an already uncanny-valley non-native experience. For developers, it means frustration, limitation, and an inability to achieve the highest-quality app experience that their businesses demand.

Skip’s belief is that by embracing native platforms wholly — not abstracting them away — is the best path forward, both for users and developers. SkipUI maps un-intermediated SwiftUI on iOS to native Jetpack Compose on Android, guaranteeing that the user experience is always performant and familiar to users of the respective platforms.

A stock Skip app has just a few core dependencies: SkipUI, which provides a bridge from the SwiftUI API to a native Compose UI on Android, along with SkipFoundation, SkipModel, and SkipLib. But Skip also facilitates a thriving ecosystem of optional libraries, providing features and integrations that unlock the vast capabilities of third-party libraries and services and provide a unified dual-platform API surface.

Throughout 2025, Skip’s library ecosystem has matured and expanded dramatically. The community and core team introduced a wide range of new dual-platform frameworks designed to solve real-world problems without compromise. Some of our most popular integrations, like SQLite, Bluetooth, Firebase, Supabase, and WebView, have improved greatly through the help of outside contributions. These APIs were refined, edge cases were resolved, documentation improved, and real production feedback shaped work on of these frameworks.

In addition, we have some newer entrants to the Skip ecosystem, including:

  • SkipNFC and SkipDevice for unlocking low-level hardware capabilities
  • SkipStripe for Stripe for payments and subscriptions
  • SkipPostHog for analytics and product insights
  • SkipAuth0 for authentication and identity
  • SkipSocketIO for real-time communication through the Socket.IO libraries

These integration frameworks aren’t always just simple wrappers; they are designed to feel idiomatic in Swift, be composable with SwiftUI, and act faithfully with each platform’s underlying capabilities. And all of these platforms work equally with with transpiled Skip Lite as well as compiled Skip Fuse. A partial list of these Skip modules can be found at the Skip Module Index.

As exciting as 2025 was, we’re even more energized by what’s ahead. Our roadmap for 2026 includes:

  • A growing catalog of integration frameworks for popular libraries, services, and backend platforms
  • Continued expansion and refinement of SkipFuse and Swift-on-Android tooling
  • Performance improvements, better diagnostics, and enhanced developer ergonomics
  • Enhanced IDE integration, both with our existing Xcode support as well as emerging alternatives for iOS development
  • A new series of deep-dive blog posts exploring real-world Skip architectures, advanced SwiftUI patterns, and platform-specific best practices

Most importantly, we’ll continue building Skip in close collaboration with the community that made this year possible. If you haven’t yet tried Skip, there’s no better time than now to sign up for your free evaluation and start creating universal mobile apps that are free from compromises.

As always, Happy Skipping, and Happy New Year!

An official Swift SDK for Android

When we first launched Skip in 2023, the notion of using Swift to create universal mobile applications was novel. While some projects had dabbled with custom Swift toolchains to share some of their business logic between iOS and Android, no one had ever undertaken the effort to enable building the entire application stack — from the low-level networking and persistence all the way up to the user interface — using just the Swift language.

But the time was right. SwiftUI was just reaching maturity, and its declarative design was flexible enough to target not only the mobile phone form factor, but also to scale all the way up to the full desktop and all the way down to the smartwatch. Expanding SwiftUI’s architecture to the “other” mobile platform was a daunting engineering challenge, but it made perfect sense from the standpoint of facilitating the creation of a whole app using a single language.

Developers who have adopted Skip for their dual-platform app development have loved it. But there has always been an undercurrent of caution and reservation about the future of the project, especially from larger enterprises for whom the architecture decisions were central to their business’ future. As we’ve written in the past, the best-in-class apps that top the charts on both the App Store and Play Store are not written once, but twice: they are written first in Swift for iOS, and then they are re-written in Kotlin for Android. Despite being enormously laborious to coordinate and maintain, writing the app twice has always been considered the safe choice, not just because it enables optimal performance and a truly native user interface, but also because they are using the languages and APIs that are recommended and supported by the operating system vendors themselves. How could an independent project by a small team possibly offer the same guarantees of technological durability?

Such concerns have presented a challenge and barrier against the adoption of Skip for cross-platform app development since the beginning. And so we joined together with some other visionaries and founded the Swift on Android Community Working Group1 earlier this year. Our goal was to collaborate in harnessing and coordinating the energies of various developers and businesses that had each dabbled in using Swift in some way for their Android apps.

The workgroup had so much excitement behind it that a few months later, it was blessed by the Swift Platform Steering Group as an official workgroup2, which meant that we had the backing and support of the Swift community as a whole. This was huge: Swift on Android was no longer a niche interest for risk-taking startups and indie developers, but was going to evolve into a fully-supported platform for the Swift language.

Work began in earnest. Since last year, Skip has been using an unofficial preview build of the toolchain and native Swift SDK for Android to power our “Skip Fuse” mode3. Using this technology as a base — which had evolved over the years in a somewhat haphazard fashion — we began the long process of getting it in shape for official approval and release: cleanup, bug fixes, ripping out unsupported dependencies, harmonizing the structure with other Swift SDKs, packaging, quality control, and continuous integration.

The culmination of all these efforts has at last arrived! As announced on the swift.org blog4, we are now publishing the Swift SDK as an officially supported platform. The Swift SDK for Android is available alongside the Static Linux (Musl) and WebAssembly (Wasm) SDKs, and will be available in nightly snapshot releases throughout the Swift 6.3 release cycle.

As mentioned, Skip is currently using our own preview release build of this SDK for our native Skip Fuse mode. So switching over to this official SDK will be smooth and painless for our current customers. We anticipate that the final Swift 6.3 release will be the point where we include it by default in the Skip installation and setup instructions.

Note that this SDK is not just theoretical, but is in active use today in many Skip-powered applications. Our own Skip Showcase5 app has been running using this SDK, and provides a comprehensive sample of what is possible when you combine native Swift with Skip’s SwiftUI implementation for Android.

Since we announced the availability of the Swift SDK for Android, there has been an explosion of interest in the project. Many heretofore skeptics are realizing that this is real, and are seeing that Swift is a viable choice as the one language for their entire application stack — on all platforms. No longer do developers need to make the agonizing choice between writing an application in two separate native languages, versus settling on an inefficient and alien language like JavaScript or Dart for their shared codebase.

For Skip itself, this development grants us an enormous amount of confidence-building support. Swift on Android is here to stay. And so even if Skip as a product were to somehow disappear tomorrow, any investment that is made in Swift for Android development would continue be a viable and supported path going forward. Swift on Android is available today, it has official backing by the Swift project, and it is here to stay. The future is bright!

Screenshot of Skip Showcase native app
Download on the Google Play Store Download on the Apple App Store
  1. Swift on Android Working Group, Community Showcase, February 10, 2025: https://forums.swift.org/t/swift-on-android-working-group/77780

  2. Announcing the Android Workgroup, June 25, 2025: https://forums.swift.org/t/announcing-the-android-workgroup/80666

  3. Native Swift on Android, Part 1: Setup, Compiling, Running, and Testing: /blog/native-swift-on-android-1/

  4. Announcing the Swift SDK for Android: https://www.swift.org/blog/nightly-swift-sdk-for-android/

  5. Skip Showcase /docs/samples/skipapp-showcase-fuse/

Skip on the Swift Package Indexing Podcast

I was thrilled to be interviewed by Dave and Sven from the Swift Package Indexing podcast the other day! We talked about a wide range of topics around the founding of the Swift Android Workgroup, the progress that Swift is making in expanding into other platforms, and how Skip builds on the Swift Android SDK to enable building both iOS and Android apps from the same SwiftUI codebase.

We also discussed some tips for Swift package developers to make their own packages compatible with Android, and talked about the recent addition of Android compatibility to the supported platforms matrix for packages on https://swiftpackageindex.com.

You can check out Episode 61: “People have been working on it for ten years” for the full transcript and show notes.

Peter and Sven are gracious hosts, and I greatly enjoyed our conversation.

Fully Native Cross-Platform Swift Apps

Screenshot

In our series on using native Swift on Android, we have covered the basics of the Swift-Android toolchain in Part 1, bridging between compiled Swift and Kotlin in Part 2, and the creation of a cross-platform app with a shared Swift model layer in Part 3.

We are pleased to unveil the culmination of all of this work: Skip 1.5 now has the ability to create a 100% native Swift and SwiftUI app for both iOS and Android! You can now enjoy the safety, efficiency, and expressiveness of pure Swift throughout your entire cross-platform app, including the ability to tap into the vast ecosystem of Swift packages to support your app development.

This blog post will go over the process of creating and developing a Swift cross-platform app, explore the underlying technologies, and discuss our current status and next steps.

Ensure that you are on a macOS 14+ machine with Xcode 16, Android Studio 2025, and Homebrew installed.

First, create and launch an Android emulator for testing.

Next, open Terminal and type the following commands to install Skip and the native Swift-Android toolchain.

$ brew install skiptools/skip/skip
$ skip upgrade
$ skip android sdk install

Verify that everything is working with an additional Terminal command:

$ skip checkup --native

You’re now ready to create your first fully native cross-platform Swift app:

$ skip init --native-app --open-xcode --appid=bundle.id.HowdySkip howdy-skip HowdySkip

Assuming the app initialized successfully, your project will open in Xcode. Run the app against an iPhone simulator destination (the first build may take some time) and your app will launch on both the iOS simulator and the running Android emulator at the same time!

The skip init command creates a template app to get you started. It is a TabView containing a “Welcome” view, a list of editable and re-orderable “Items”, and a “Settings” view:

Screenshot of the Howdy Swift native app Welcome Tab

Rather than importing SwiftUI, you will notice that the Sources/HowdySkip/ContentView.swift file instead imports SkipFuseUI. When building for iOS, this simply redirects to a SwiftUI import. Skip’s philosophy is to not intrude on the iOS side of the application: on Darwin platforms you are still using direct, non-intermediated SwiftUI, just as if you were building an app without Skip at all.

On Android, however, the SkipFuseUI module bridges the SwiftUI API onto Jetpack Compose, Android’s modern Kotlin UI toolkit. It does this through the intermediate SkipUI module. Bridging to pure Jetpack Compose gives Android users a fully native user experience rather than the uncanny-valley replica generated by many cross-platform frameworks.

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

Consult the SkipUI module’s documentation for a listing of currently-supported SwiftUI constructs on Android. You can also examine the ShowcaseFuse cross-platform sample app, which displays and exercises most supported SwiftUI components:

Screen recording

The “Items” tab displays an editable list of items which are managed by Sources/HowdySkip/ViewModel.swift, which handles loading and saving the list of items to a simple JSON file. This code uses standard Foundation types (URL, Data, Date, FileManager, JSONEncoder, JSONDecoder, etc.) to handle the management and persistence of the items. Note that unlike the SwiftUI in ContentView, none of this code is bridged into Kotlin: it is using the Apple swift-foundation types directly, just as on iOS.

Screenshot of the Howdy Swift native app List Tab

Despite only using native Foundation types, you will notice that ViewModel.swift imports SkipFuse. Just as SkipFuseUI bridges your UI to Android, SkipFuse bridges model-layer code. We use it here to enable our @Observable view model to communicate changes to the Jetpack Compose user interface. This is discussed further in Part 2 of the series. You generally don’t need to be concerned with the details other than to remember to import SkipFuse (or SkipFuseUI) any time you implement an @Observable.

The final tab of the sample app is the “Settings” screen. This exposes various settings and displays some information about the app. It also presents a little heart emoji, which is blue on iOS and green on Android.

Screenshot of the Howdy Swift native app Settings Tab

We use the green heart emoji to demonstrate a powerful feature of Skip: the ability to embed code that directly calls Kotlin and Jetpack Compose APIs! Examining the SettingsView in ContentView.swift, you will see the inclusion of a PlatformHeartView, whose implementation looks like this:

/// A view that shows a blue heart on iOS and a green heart on Android.
struct PlatformHeartView : View {
var body: some View {
#if !os(Android)
Text(verbatim: "💙")
#else
ComposeView {
HeartComposer()
}
#endif
}
}
#if SKIP
/// Use a ContentComposer to integrate Compose content. This code will be transpiled to Kotlin.
struct HeartComposer : ContentComposer {
@Composable func Compose(context: ComposeContext) {
androidx.compose.material3.Text("💚", modifier: context.modifier)
}
}
#endif

What is going on here? Notice that on Android, we’re rendering the heart with a ComposeView, a special SwiftUI view for including Jetpack Compose content in the form of a ContentComposer.

/// Encapsulation of Composable content.
public protocol ContentComposer {
@Composable func Compose(context: ComposeContext)
}

But how can we define @Composable functions and call Android APIs from within our native Swift code?

The magic lies in Skip’s ability to transpile Swift code into Kotlin, and SkipFuse’s ability to bridge between Kotlin and your compiled Swift. Any code in a #if SKIP block will be transformed into the equivalent Kotlin, so it is free to call other Kotlin and Java APIs directly. The generated #if SKIP Kotlin will also be automatically bridged so that you can call it from your native Swift.

Diagram of Skip's custom Compose view embedding

Screen recording

The ability to effectively embed Kotlin code is immensely powerful. It not only provides direct access Jetpack Compose and the Android SDK, but also enables you to tap into the complete Android ecosystem of libraries through Kotlin and Java dependencies.

Of course, using native Swift allows you to take advantage of the vast and growing ecosystem of available Swift packages as well. In our post on Bringing Swift Packages to Android we introduced the swift-everywhere.org site that tracks packages that are successfully building for Android. Since that post, community members have been implementing Android support, bringing the number of known packages that can be used on Android to 2,240! Popular projects like Alamofire, flatbuffers, SwiftSoup, swift-protobuf, and swift-sqlcipher (just to name a few) can be added directly to your app and used in the same way on both iOS and Android.

Skip’s unique ability to directly call both Swift and Kotlin/Java APIs separates it from most cross-platform development frameworks, where integrating with the host system often requires bespoke adapters and extra layers of indirection.

As a demonstration and validation of this technology, we have published one of our sample apps, Skip Notes, to both the Google Play Store and Apple App Store. This fully native Swift app integrates with the swift-sqlcipher database library to provide persistence for a simple list of notes.

Get it on the Google Play Store Get it on the Apple App Store {: .centered }

Despite being generally available, Skip’s native support is currently a technology preview. We are working on updating our documentation, finding and squashing remaining bugs and limitations, reducing build times, and generating smaller Android binaries. Even as a preview, however, you can build complete, production-ready cross-platform Swift apps, as Skip Notes demonstrates.

Cross-platform Swift and SwiftUI have been a dream of ours for a long time. We are immensely excited about the possibilities this unlocks for creating truly best-in-class apps for both iOS and Android from a single codebase!

Swift Everywhere: Bringing Swift Packages to Android

Swift Android Logo {: style=“text-align: center; width: 200px; margin: auto;”}

  • Table of contents {:toc}

In recent weeks, the Skip team has submitted patches to numerous Swift projects to add Android support to their packages. We’ve been tracking the progress of Android build-ability on our swift-everywhere.org web site, which catalogs a list of many popular Swift packages and whether they compile for Android. At the time of writing, nearly two thousand Swift packages are building successfully for Android, with more being added every day.

This article will go over what our experience porting Swift packages has taught us, and how you can apply this knowledge to turn your parochial Apple-only Swift package into a universal multi-platform package that can build for not just iOS and macOS, but also for Android.

What sorts of Swift packages are good candidates for Android? The best litmus test is whether the package offers general-purpose functionality, as opposed to having an integral dependency on iOS-specific frameworks. Some examples of good candidates are:

  • Business logic
  • Algorithms and generic data structures
  • Networking utilities
  • Online web service and API clients
  • Data persistence
  • Parsers and formatters

On the flip side, examples of packages that would be challenging to bring to Android might be:

  • Custom UIKit components
  • HealthKit, CarPlay, Siri integration libraries
  • Other Apple-specific *Kit library integrations

Now, just because a Swift package is designed to work with an Apple-specific framework doesn’t mean that it is impossible to port to Android. It just means that it would be a signifiant amount of work and involve creating a bridge to the equivalent Kotlin or Java framework. This is by all means possible – and will be the topic of a future post – but the subject of this article is how to bring naturally portable Swift packages to Android.

Say you have a conventional Swift package that contains a Package.swift file at the root and has the usual Sources/ and (hopefully) Tests/ folders that contain the individual targets and source files.

Does running swift build and swift test in the package directory work from the Terminal? If so, then you already have a cross-platform package that works on multiple platforms: iOS and macOS! That, by itself, is a good sign that your package might be suitable for Android. Many frameworks that are available on iOS are not present on macOS, so either your package doesn’t use too many iOS frameworks, or it is smart enough to only reference them conditionally (more on that below). But how do we build and test for Android?

First, install Skip and the native Android SDK by following the instructions in our documentation. Then try to build your Swift package with the Android toolchain. The very abbreviated quick start looks like:

$ brew install skiptools/skip/skip
skip was successfully installed!
$ skip android sdk install
[✓] Install Swift Android SDK (2.4s)
$ cd MySwiftPackage/
$ skip android build
Building for debugging...
[0/2] Write sources
[4/4] Emitting module DemoPackage
Build complete! (1.85s)

If you see “Build complete!” then congratulations! Your package already builds for Android, and you can move on to the Testing section. But if you encounter errors from the build command, you will need to port your package over to Android. Read on…

Wikipedia defines porting as the “process of adapting software for the purpose of achieving some form of execution in a computing environment that is different from the one that a given program (meant for such execution) was originally designed for”.

In other words, you made your Swift package with iOS in mind, and now you want it to work on Android. The following sections will go over some of the most common issues you may hit when first trying to build your package on this new platform.

Conditionally Importing and Using Platform-Specific Modules

Section titled “Conditionally Importing and Using Platform-Specific Modules”

Suppose your Swift package defines an Event protocol with a simple default implementation:

protocol Event {
var dateRange: Range<Date> { get }
var isConfirmed: Bool { get }
}
struct SimpleEvent : Hashable, Codable {
let start, end: Date
let confirmed
}
extension SimpleEvent : Event {
var dateRange: Range<Date> { self.start..<self.end }
var isConfirmed: Bool { confirmed }
}

Your package also includes an extension to implement Event using the iOS EventKit framework, like so:

import EventKit
extension EKEvent : Event {
var dateRange: Range<Date> { self.startDate..<self.endDate }
var isConfirmed: Bool { self.status == .confirmed }
}

EventKit is an Apple-only framework, so when you try to build the package for Android, you will hit an error:

$ skip android build
7 | import EventKit
| `- error: no such module 'EventKit'

The solution to this is simple: wrap any code that references the missing module in #if canImport(EventKit), which conditionally compiles the code only when the specified module is available:

protocol Event {
var dateRange: Range<Date> { get }
var isConfirmed: Bool { get }
}
struct SimpleEvent : Hashable, Codable {
let start, end: Date
let confirmed
}
extension SimpleEvent : Event {
var dateRange: Range<Date> { self.start..<self.end }
var isConfirmed: Bool { confirmed }
}
#if canImport(EventKit)
import EventKit
extension EKEvent : Event {
var dateRange: Range<Date> { self.startDate..<self.endDate }
var isConfirmed: Bool { self.status == .confirmed }
}
#endif

Now you will have all the general functionality of the package available to Android, which you can adapt for any Android-specific data structures you may create in the future.

Consider the following simple utility that fetches a URL and decodes it into an Item struct:

import Foundation
struct Item: Decodable {
let id: Int
let name: String
}
func fetch(_ url: URL) async throws -> Item {
let (data, response) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(Item.self, from: data)
}

You may be surprised to see this fail to compile for Android:

$ skip android build
Building for debugging...
Fetcher.swift:9:49: error: type 'URLSession' (aka 'AnyObject') has no member 'shared'
7 |
8 | func fetch(_ url: URL) async throws -> Item {
9 | let (data, response) = try await URLSession.shared.data(from: url)
| `- error: type 'URLSession' (aka 'AnyObject') has no member 'shared'
10 | return try JSONDecoder().decode(Item.self, from: data)
11 | }

This somewhat confusing error message just means that it can’t find the URLSession type, because it isn’t present in the Foundation module on Android.

On Darwin platforms (macOS, iOS, and other Apple operating systems), the Foundation module is an umbrella for a wide variety of functionality. But on other platforms, such as Android and Linux, Foundation is broken up into multiple separate sub-components:

  • FoundationEssentials: All the basic Foundation types: Date, Calendar, URL, IndexSet, etc.
  • FoundationInternationalization: DateFormatter, NumberFormatter, and other localization utilities
  • FoundationNetworking: URLSession, URLCache, and other networking utilities
  • FoundationXML: XMLParser

The solution to this is simple: add a conditional import of FoundationNetworking to any file that uses any networking functionality, like so:

import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
struct Item: Decodable {
let id: Int
let name: String
}
func fetch(_ url: URL) async throws -> Item {
let (data, response) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(Item.self, from: data)
}

This will include the required FoundationNetworking module for platforms like Android and Linux that need it, but quietly ignore it on Darwin platforms like iOS and macOS where the networking types are included with the monolithic Foundation framework.

Swift has excellent integration with C, and many useful functions come in from the system’s C library, which is called Darwin on macOS and iOS. Take the simple example of calculating the hypotenuse of a triangle, which uses some math functions brought in from the standard C library:

import Darwin
func hypotenuse(a: Double, b: Double) -> Double {
return sqrt(pow(a, 2) + pow(b, 2))
}

If you try to build this for Android, you will hit the error:

1 | import Darwin
| `- error: no such module 'Darwin'

This is because the Darwin module doesn’t exist for Android. It is instead simply called Android. Again, we solve this with our handy conditional canImport:

#if canImport(Darwin)
import Darwin
#elseif canImport(Android)
import Android
#else
#error("Unknown platform")
#endif
func hypotenuse(a: Double, b: Double) -> Double {
return sqrt(pow(a, 2) + pow(b, 2))
}

In this case, we import either Darwin on iOS and macOS, or Android on Android. Both of these will provide access to the system’s standard C library.

Simple C functions (like pow and sqrt) will generally be surfaced in exactly the same way on Darwin and Android platforms. But the definition of some functions and data structures in the Android C library can sometimes differ in subtle ways. For example, the following code uses the FILE type and fopen and fwrite C functions on Darwin platforms:

import Darwin
let fd: UnsafeMutablePointer<FILE> = fopen("file.txt", "w")
var buffer: [UInt8] = [1, 2, 3]
let count: Int = buffer.withUnsafeBufferPointer { ptr in
fwrite(ptr.baseAddress, MemoryLayout<UInt8>.stride, ptr.count, fd)
}

This will fail to build for Android:

$ skip android build
FileWrite.swift:15:30: error: cannot find type 'FILE' in scope
13 | #endif
14 |
15 | let fd: UnsafeMutablePointer<FILE> = fopen("file.txt", "w")
| `- error: cannot find type 'FILE' in scope
16 | var buffer: [UInt8] = [1, 2, 3]
17 | let count: Int = buffer.withUnsafeBufferPointer { ptr in
FileWrite.swift:18:16: error: value of optional type 'UnsafePointer<UInt8>?' must be unwrapped to a value of type 'UnsafePointer<UInt8>'
16 | var buffer: [UInt8] = [1, 2, 3]
17 | let count: Int = buffer.withUnsafeBufferPointer { ptr in
18 | fwrite(ptr.baseAddress, MemoryLayout<UInt8>.stride, ptr.count, fd)
| |- error: value of optional type 'UnsafePointer<UInt8>?' must be unwrapped to a value of type 'UnsafePointer<UInt8>'
| |- note: coalesce using '??' to provide a default when the optional value contains 'nil'
| `- note: force-unwrap using '!' to abort execution if the optional value contains 'nil'
19 | }
20 |

There are two separate issue here:

  • FILE doesn’t exist on Android, so UnsafeMutablePointer<FILE> must be replaced with OpaquePointer
  • Functions like fwrite that take a file pointer will not accept an optional, and so must be force-unwrapped from their pointer’s rawValue

The following conditional typealias will handle the first issue, and simply force-unwrapping the pointer’s address (which should be valid on all platforms) addresses the second:

#if canImport(Darwin)
import Darwin
#elseif canImport(Android)
import Android
#else
#error("Unknown platform")
#endif
#if os(Android)
typealias Descriptor = OpaquePointer
#else
typealias Descriptor = UnsafeMutablePointer<FILE>
#endif
let fd: Descriptor = fopen("file.txt", "w")
var buffer: [UInt8] = [1, 2, 3]
let count: Int = buffer.withUnsafeBufferPointer { ptr in
fwrite(ptr.baseAddress!, MemoryLayout<UInt8>.stride, ptr.count, fd)
}

Unless you are developing very low-level code that interfaces with the platform’s C library, you will rarely encounter these sorts of issues. But when you do, it is good to know that the solutions tend the be fairly simple. The most difficult part is often just deciphering the compilation failure message.

So now your package builds for Android with the command: skip android build. Amazing!

But you are only halfway there: you need to make sure your code not only builds for Android, but that it actually works. Hopefully, your Swift package includes test cases in the Test/ folder, and running the tests locally on your macOS machine with swift test works. For example, with the swift-algorithms package:

$ swift test
Building for debugging...
[78/78] Linking swift-algorithmsPackageTests
Build complete! (12.67s)
Test Suite 'All tests' started at 2025-01-21 19:25:03.841.
Test Suite 'swift-algorithmsPackageTests.xctest' started at 2025-01-21 19:25:03.842.
Test Suite 'AdjacentPairsTests' started at 2025-01-21 19:25:03.842.
Test Case '-[SwiftAlgorithmsTests.AdjacentPairsTests testEmptySequence]' started.
Test Case '-[SwiftAlgorithmsTests.AdjacentPairsTests testEmptySequence]' passed (0.002 seconds).
Test Case '-[SwiftAlgorithmsTests.AdjacentPairsTests testIndexTraversals]' started.
Test Case '-[SwiftAlgorithmsTests.AdjacentPairsTests testIndexTraversals]' passed (0.002 seconds).
Test Suite 'All tests' passed at 2025-01-21 19:25:05.718.
Executed 212 tests, with 0 failures (0 unexpected) in 1.870 (1.876) seconds

In order to run your tests on Android, you will need to either plug in an Android device (with USB debugging enabled), or else configure and launch an Android emulator, either from the command line or Android Studio).

Once you have your Android development target setup, you can run your package’s test cases with the skip android test command, which will compile the test cases, bundle them up (along with any associated resources), copy them to the Android device or emulator, and then execute the test cases remotely.

For example, for the swift-algorithms package:

% skip android test
[0/1] Planning build
Building for debugging...
[83/84] Linking swift-algorithmsPackageTests.xctest
Build complete! (11.68s)
[✓] Check Swift Package (0.87s)
[✓] Connecting to Android (0.18s)
[✓] Copying test files (0.88s)
Test Suite 'All tests' started at 2025-01-21 21:02:09.086
Test Suite 'swift-algorithms-1C77777B-CEC3-4075-8853-E77CECFCF30B.xctest' started at 2025-01-21 21:02:09.105
Test Suite 'AdjacentPairsTests' started at 2025-01-21 21:02:09.105
Test Case 'AdjacentPairsTests.testEmptySequence' started at 2025-01-21 21:02:09.105
Test Case 'AdjacentPairsTests.testEmptySequence' passed (0.014 seconds)
Test Case 'AdjacentPairsTests.testIndexTraversals' started at 2025-01-21 21:02:09.120
Test Case 'AdjacentPairsTests.testIndexTraversals' passed (0.004 seconds)
Test Suite 'All tests' passed at 2025-01-21 21:02:21.697
Executed 212 tests, with 0 failures (0 unexpected) in 12.579 (12.579) seconds

If there are any test failures, this is where you will delve into the details of your test case, isolate the problem, and apply fixes. There are many reasons why tests may fail, such as assumptions about the filesystem layout. These will need to be examined and resolved on a case-by-case basis.

Once all your tests pass, you’ve successfully brought your Swift package to Android!

Once you have your package building and your tests passing, you will want to ensure that they continue to pass. Maintaining a package that supports multiple platforms can be more challenging than just a single platform, because often when a new feature is implemented or a bug is fixed, the change will only be tested on the platform the developer is currently working with. For example, if you are working on the iOS side of your application and make a bug fix in one of your packages, you may only test the changes on that one platform, but it may inadvertently break something on another platform.

This is where continuous integration (CI) can be useful. If you use GitHub as your package’s source code management system, you can utilize GitHub Actions to automatically build and test your package on multiple platforms whenever you push to the repository or, for example, whenever a pull request is created from a branch or fork.

In order to facilitate Android CI, we provide the swift-android-action, which enables you to build and test your package against Android in a single line of configuration.

The following example of a .github/workflows/ci.yml script will build and test your package on each of macOS, iOS, Linux, and Android whenever a commit is pushed or a PR is created:

name: swift package ci
on:
push:
branches:
- '*'
workflow_dispatch:
pull_request:
branches:
- '*'
jobs:
linux-android:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: "Test Swift Package on Linux"
run: swift test
- name: "Test Swift Package on Android"
uses: skiptools/swift-android-action@v2
macos-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- name: "Test Swift Package on macOS"
run: swift test
- name: "Test Swift Package on iOS"
run: xcodebuild test -sdk "iphonesimulator" -destination "platform=iOS Simulator,name=iPhone 15" -scheme "$(xcodebuild -list -json | jq -r '.workspace.schemes[-1]')"

You can see this workflow in play in many of the packages on GitHub that support Android, such as Skip’s own swift-sqlcipher package.

In this way, you can be assured that once you have done the hard work of getting your package working with Android, it continues to work on all your supported platforms.

Expanding your Swift packages to support platforms beyond iOS may at first seem daunting, but using the advice from this article, you can follow a few simple steps that will put you on the right track:

  1. Setup Skip and the Swift Android SDK
  2. Try to build your package with skip android build
  3. Identify build errors and resolve them with conditional imports and by accommodating platform differences
  4. Set up an Android emulator or device for testing
  5. Test your package with skip android test
  6. Identify test failures and resolve them on a case-by-case basis

This is the sequence we have used to add Android support to dozens of popular Swift packages, such as GraphQL, CryptoSwift, and PromiseKit. With nearly 2,000 Swift packages currently building for Android, we feel the platform has achieved enough critical mass to make Swift an attractive language for many parts of your apps on both major mobile platforms: iOS and Android. And if you have a popular GitHub package that builds for Android, expect it to show up at swift-everywhere.org in the near future!

To learn more about running Swift on Android and how it integrates with Skip’s tools for creating dual-platform mobile apps, please see our ongoing blog series on the topic:

Native Swift on Android, Part 3: Sharing a Swift Model Layer

Native Swift on Android, Part 3: Sharing a Swift Model Layer

This is the third installment in our series exploring native Swift on Android. In Part 1 we discuss bringing the Swift compiler and toolchain to Android. Part 2 introduces Skip’s tooling and technology for Swift Android app development and leads you through the creation of your first cross-platform Swift app.

The app we create in Part 2 uses a compiled Swift model layer and a shared SwiftUI interface, which Skip transpiles to Jetpack Compose on Android. The following diagram illustrates this dual-platform, single-codebase architecture:

Skip Native Diagram {: .diagram-vector }

In this article, by contrast, we create separate iOS and Android apps. The iOS app and shared model layer are written in Swift and SwiftUI using Xcode. The Android app is written in Kotlin and Jetpack Compose using Android Studio, and it imports the compiled Swift model as a dependency. This structure allows you to reuse the lower layers of your app logic while fully embracing the standard IDEs and UI toolkits on each platform:

Skip Shared Model Diagram {: .diagram-vector }

Simulators displaying the same app on iPhone and Android

Our sample apps in this installment are iOS and Android versions of TravelPosters, a simple scrolling grid displaying posters of famous cities. Each poster displays the city’s name and current temperature. You can mark your favorites, and these favorites are remembered across app launches.

Our shared TravelPostersModel, therefore, has the following responsibilities:

  • Provide a list of cities. Each city must supply its name and a poster image URL.
  • Fetch the current temperature for each city.
  • Allow the addition and removal of cities from an observable set of favorites.
  • Persist and restore the set of favorites across uses of the app.

And given that our model will power both iOS and Android apps, we should add the following table-stakes Android requirements:

  • We must be able to access our Swift model API naturally in Kotlin, just as in Swift.
  • Our mutable set of favorites must be observable not only to SwiftUI state tracking, but to Jetpack Compose state tracking as well.

Fortunately, Swift is more than up to the task of meeting our model’s general requirements, and Skip’s SkipFuse technology will handle transparently bridging it all to Kotlin and Compose!

If you plan on following along and you haven’t already installed Skip, follow Part 2’s installation instructions. This will quickly get you up and running with Skip, its requirements, and the native Swift Android toolchain.

As a good, modern citizen of the Swift ecosystem, Skip works atop Swift Package Manager. Our shared model will be a Swift package configured to use skipstone, the Skip build plugin. You could create this package and configure its use of Skip by hand, but Skip provides tooling to help.

First, create the folder structure we’ll use to hold our shared model as well as our iOS and Android apps. You do not have to house your apps together, but this is the structure we’ll use in this article.

mkdir travelposters
cd travelposters
mkdir iOS
mkdir Android

Now use the skip tool to create the shared model package:

skip init --native-model travel-posters-model TravelPostersModel
Output of running skip init --native-model

This command generates a travel-posters-model SwiftPM package containing the TravelPostersModel Swift module. The --native-model option ensures that the module will already be configured to compile natively on Android, and to bridge its public API to Kotlin. Our particular needs, however, require a couple of additional steps.

  1. We know that parts of our model will be @Observable. In order for @Observables to work on Android, we need a dependency on skip-model. Edit the generated Package.swift to add it:

    ...
    let package = Package(
    name: "travel-posters-model",
    ...
    dependencies: [
    .package(url: "https://source.skip.tools/skip.git", from: "1.2.0"),
    .package(url: "https://source.skip.tools/skip-model.git", from: "1.0.0"), // <-- Insert
    .package(url: "https://source.skip.tools/skip-fuse.git", "0.0.0"..<"2.0.0")
    ],
    targets: [
    .target(name: "TravelPostersModel",
    dependencies: [
    .product(name: "SkipFuse", package: "skip-fuse"),
    .product(name: "SkipModel", package: "skip-model") // <-- Insert
    ],
    plugins: [.plugin(name: "skipstone", package: "skip")]),
    ...
    ]
    )
  2. The --native-model option we passed to skip init will configure Skip to automatically bridge our model’s public API from compiled Swift to Android’s ART Java runtime. This is done through the skip.yml configuration file included in every Skip module. By default, however, Skip assumes that you’ll be bridging to transpiled Swift and SwiftUI code. Instead, we’ll be consuming the model from pure Kotlin, so we want to optimize the bridging for Kotlin compatibility. We do this by editing the Sources/TravelPostersModel/Skip/skip.yml file to look like this:

    skip:
    mode: 'native'
    bridging:
    enabled: true
    options: 'kotlincompat'

You can read more about the magic of bridging in the documentation.

With these updates in place, we’re now ready to iterate on our shared Swift model code!

The beauty of cross-platform Swift code is how boring it is. You can browse our model’s complete content on GitHub, but it looks more or less exactly as you’d expect given the previously-enumerated requirements. It has some Codable structs to represent cities and weather:

public struct City : Identifiable, Codable {
public typealias ID = Int
public let id: ID
public let name: String
public let imageURL: URL
...
}
public struct WeatherConditions : Hashable, Codable {
public let temperature: Double // 16.2
public let windspeed: Double // 16.6
...
}

The model uses URLSession and JSONDecoder to fetch the current weather:

public struct Weather : Hashable, Codable {
public let latitude: Double // e.g.: 42.36515
public let longitude: Double // e.g.: -71.0618
public let time: Double // e.g.: 0.6880760192871094
...
public let conditions: WeatherConditions
enum CodingKeys: String, CodingKey {
case latitude = "latitude"
case longitude = "longitude"
case time = "generationtime_ms"
...
case conditions = "current_weather"
}
public static func fetch(latitude: Double, longitude: Double) async throws -> Weather {
let factor = pow(10.0, 4.0) // API expects a lat/lon rounded to 4 places
let lat = Double(round(latitude * factor)) / factor
let lon = Double(round(longitude * factor)) / factor
let url = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=\(lat)&longitude=\(lon)&current_weather=true")!
var request = URLRequest(url: url)
request.setValue("skipapp-sample", forHTTPHeaderField: "User-Agent")
let (data, response) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(Weather.self, from: data)
}
}

And it includes an @Observable CityManager to provide the list of cities and to persist favorites:

@Observable public final class CityManager {
private static let favoritesURL = URL.applicationSupportDirectory.appendingPathComponent("favorites.json")
public static let shared = CityManager()
private init() {
do {
self.allCities = try JSONDecoder().decode([City].self, from: localCitiesJSON.data(using: .utf8)!).sorted { c1, c2 in
c1.name < c2.name
}
} catch {
logger.log("error loading cities: \(error)")
}
do {
self.favoriteIDs = try JSONDecoder().decode([City.ID].self, from: Data(contentsOf: Self.favoritesURL))
logger.log("loaded favorites: \(self.favoriteIDs)")
} catch {
logger.log("error loading favorites: \(error)")
}
}
public var allCities: [City] = []
public var favoriteIDs: [City.ID] = [] {
didSet {
logger.log("saving favorites: \(self.favoriteIDs)")
do {
try FileManager.default.createDirectory(at: Self.favoritesURL.deletingLastPathComponent(), withIntermediateDirectories: true)
try JSONEncoder().encode(favoriteIDs).write(to: Self.favoritesURL)
} catch {
logger.log("error saving favorites: \(error)")
}
}
}
}
private let localCitiesJSON = """
...
"""

While this code is generally pretty standard, it does contain a few concessions to the realities of current Swift support on Android:

  • In files that create an OSLog.Logger or that define an @Observable type, we also import SkipFuse. In fact, Skip will surface a build warning in Xcode if you attempt to define an @Observable in a bridged file that doesn’t import the SkipFuse framework!

    SkipFuse is an umbrella framework that “fuses” the Swift and Android worlds. It makes sure that your OSLog messages are routed to Android’s Logcat logging service, that your @Observable state is tracked by Jetpack Compose, and more - all without changes to your normal code path.

  • You may notice other unfamiliar import patterns as well. For example, Foundation on Linux and Android is divided into Foundation, FoundationNetworking, FoundationInternationalization, and FoundationXML. So in Weather.swift where we use URLSession, we have the following imports:

    import Foundation
    #if canImport(FoundationNetworking)
    import FoundationNetworking
    #endif
  • Though we do not need them here, you may encounter #if os(Android) checks to conditionalize code for Android or Darwin platforms in other Android-supporting codebases, just as you’ll often find #if os(macOS) conditions in macOS-supporting codebases.

  • We’re loading our cities JSON from a static string, but more tyically you would load the contents from a resource. SkipFuse supports bundling Swift module resources as idiomatic Android assets.

  • While many Swift packages like Apple’s swift-algorithms compile cleanly for Android out of the box, others will require minor changes, and still others - particularly those that tie into the hardware or use one of Apple’s many OS “Kits” - may never work on Android. Swift on Android is still in its infancy, and it will take time for developers to build and test their packages on this new-to-Swift platform.

You can read much more about both the advantages and the limitations of native Swift on Android in our full native Swift documentation. For the most part, though, relax and enjoy coding with the full power and expressiveness of Swift!

Due to limitations on build plugins, building the travel-posters-model package in Xcode does not perform an Android build. It only builds for iOS. Rather, there are two simple ways to build for Android: use skip export to create an Android library archive, which we explore later in this article, or run the unit tests.

Skip configures every native module with an extra unit test that builds the module for Android, transpiles your XCTests to JUnit tests, and runs them. Thus you’ll see two sets of results on every test run: first from XCTest and then from JUnit on Android. Frequently running your tests is a great way to catch both logic bugs and Android compilation errors early. Read more in the native testing documentation.

Because our model is a standard SwiftPM package, you incorporate and use it on iOS like any other package. We briefly outline the steps we took to create and configure our sample iOS app below. Feel free to skip this section!

  1. Use Xcode to create a new Workspace in the travelposters directory alongside the travel-posters-model package.

  2. Use Xcode to create a new App project in the travelposters/iOS directory. Close the project after creating it, because we’re going to add it to our Workspace instead.

    Creating a new app project in Xcode
  3. Add the travel-posters-model package to your Workspace.

  4. Add the iOS/TravelPosters/TravelPosters.xcodeproj app to your Workspace.

  5. Add a package dependency from the app to the travel-posters-model local package.

    Adding a package dependency in Xcode

You can now use your Xcode Workspace to iterate on both the shared model package and your iOS app. Browse the complete iOS TravelPosters app here.

We create our TravelPosters Android app using Android Studio, starting with the “Empty Activity” template. Tell Android Studio to place the app in our travelposters/Android folder.

Creating a new app project in Android Studio

Next, make Android/lib, Android/lib/debug, and Android/lib/release directories. This is where we’ll place our compiled Swift model and Skip libraries.

Creating directories for our compiled Swift model

We must also configure our project to use the new lib directories. Edit the app module’s build.gradle.kts file to add these and other necessary dependencies:

...
dependencies {
...
implementation("org.jetbrains.kotlin:kotlin-reflect:2.1.0") // For reflection used by Skip
implementation("io.coil-kt:coil-compose:2.7.0") // For AsyncImage used to display posters
debugImplementation(fileTree(mapOf(
"dir" to "../lib/debug",
"include" to listOf("*.aar", "*.jar"),
"exclude" to listOf<String>()
)))
releaseImplementation(fileTree(mapOf(
"dir" to "../lib/release",
"include" to listOf("*.aar", "*.jar"),
"exclude" to listOf<String>()
)))
}

To prevent errors in the deployed app, include the following in build.gradle.kts as well:

android {
packaging {
jniLibs {
// doNotStrip is needed to prevent errors like: java.lang.UnsatisfiedLinkError: dlopen failed: empty/missing DT_HASH/DT_GNU_HASH in "/data/app/…/base.apk!/lib/arm64-v8a/libdispatch.so" (new hash type from the future?) (see: https://github.com/finagolfin/swift-android-sdk/issues/67)
keepDebugSymbols.add("**/*.so")
}
}
}

Finally, our app needs internet access permissions to fetch weather and display remote images. Update its AndroidManifest.xml file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET" />
...
</manifest>

Find the complete TravelPosters Android app here. The next sections detail how to export our shared model to the Android app and how to use it from our Kotlin code.

We’ve configured our Android app to look in the Android/lib/debug and Android/lib/release folders for our model, but how do we populate these folders?

The skip export command generates Android archives of a target Swift package and all of its dependencies. It has many options, which you can explore with skip export help. The following Terminal command builds our travel-posters-model and its dependencies for Android in debug mode and places the resulting .aar library archives in the Android/lib/debug directory:

skip export --project travel-posters-model -d Android/lib/debug/ --debug

To generate release archives instead:

skip export --project travel-posters-model -d Android/lib/release/ --release
Syncing Android Studio

There are many ways to automate this process, from simple scripting to git submodules to publishing the Android travel-posters-model output to a local Maven repository. Use whatever system fits your team’s workflow best.

For example, to re-build and re-launch the app after making changes to the Swift code, you might run:

Terminal window
skip export --project travel-posters-model -d Android/lib/debug/ --debug
gradle -p Android installDebug
adb shell am start -a android.intent.action.MAIN -c android.intent.category.LAUNCHER -n tools.skip.travelposters/tools.skip.travelposters.MainActivity

Now that we’ve set up the Android app to depend on our shared Swift model, what is it like to actually use the model in Kotlin and Compose code? The answer is that - thanks to SkipFuse bridging - it’s surprisingly natural!

Before we dive into using our model, though, we have to make a single call in our Android app’s main Activity to initialize integration. Skip has extended Foundation.ProcessInfo for this purpose:

MainActivity.kt
...
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
skip.foundation.ProcessInfo.launch(context = this) // <-- INSERT
enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT))
setContent {
...
}
}
...
}

No additional changes to Android’s normal startup code path are needed.

This article is not a tutorial on using Jetpack Compose. Rather, we will focus on the places where our Android UI interacts with Swift, starting with the CityList function for displaying the scrolling list of posters:

...
import travel.posters.model.CityManager // <-- 1
@Composable
fun CityList(...) {
val cityManager = CityManager.shared // <-- 2
LazyVerticalGrid(...) {
for (city in cityManager.allCities) { // <-- 3
item {
CityPoster(city, isFavorite = { cityManager.favoriteIDs.contains(city.id) }, setFavorite = { isFavorite ->
// 4
val favoriteIDs = cityManager.favoriteIDs.toMutableList()
if (isFavorite && !favoriteIDs.contains(city.id)) {
favoriteIDs.add(city.id)
cityManager.favoriteIDs = favoriteIDs
} else if (!isFavorite) {
favoriteIDs.remove(city.id)
cityManager.favoriteIDs = favoriteIDs
}
})
}
}
}
}

We’ve annotated the code above with four numbered comments. Let’s explain each:

  1. Our TravelPostersModel module is exposed to Kotlin in the travel.posters.model package. Skip simply divides your CamelCase Swift module names into ”.”-separated Kotlin package names. Single-word packages are reserved in Kotlin, so if your module name consists of a single word, Skip appends “.module”. For example, module Util turns into Kotlin package util.module.
  2. Your Swift types and API have equivalent names and signatures in Kotlin.
  3. The Swift CityManager.allCities property of type [City] bridges to a Kotlin kotlin.collections.List<City>. Consult the bridging reference to learn more about specific type mappings.
  4. Here we’re performing standard Compose state hoisting to manage the favorites list. Notice that we simply update our model - we do not explicitly trigger a change to the UI. Like SwiftUI, Compose automatically reacts to change in observed state, and SkipFuse ensures that our @Observable CityManager is fully and transparently integrated in Compose state tracking.

Each item in the city list is a CityPoster. Let’s examine that function as well:

...
import travel.posters.model.City
import travel.posters.model.Weather
@Composable
fun CityPoster(city: City, isFavorite: () -> Boolean, setFavorite: (Boolean) -> Unit) {
Box {
val url = city.imageURL // <-- 1
AsyncImage(...)
...
Column(...) {
Row(...) {
...
Icon(imageVector = Icons.Filled.Star,
modifier = Modifier.clickable {
setFavorite(!isFavoriteState.value)
}
)
...
Text(text = city.name, ...)
...
Box {
...
LaunchedEffect(city.id, degrees) {
try { // <-- 2
val c = Weather.fetch( // <-- 3
latitude = city.latitude,
longitude = city.longitude
).conditions.temperature
...
} catch (exception: Exception) {
Log.e("TravelPosters", "Error fetching weather: $exception")
}
}
...
}
}
}
}
}

Once again, we’ve added numbered comments to points of interest in the code above:

  1. In addition to bridging your own types as well as built-in types like numbers, strings, arrays, and dictionaries, SkipFuse translates common Foundation types like Data, Date, URL, and UUID to their Kotlin equivalents. In this case the City.imageURL property of type Foundation.URL maps to a java.net.URI. Again, see the bridging reference for details.
  2. Our Weather.fetch Swift function is marked throws. If the native call produces an error, the bridged Kotlin call with throw a standard Kotlin exception.
  3. Weather.fetch is an async Swift function. Skip therefore generates a Kotlin suspend function and integrates the call with Kotlin coroutines. Hence the use of a LaunchedEffect in our Compose code.

As you can see, you invoke your Swift APIs naturally in Kotlin - almost exactly as if they were written in Kotlin themselves! Swift custom types, built-in types, and common Foundation types all translate to Kotlin/Java equivalents, thrown errors cause Kotlin exceptions, async Swift functions use Kotlin coroutines, etc. The goal is that using a module written in Swift should be almost indistinguishable from using a package written in Kotlin.

If you haven’t already, check out Part 1 and especially Part 2 of this series.

If you’d like to learn much more about SkipFuse, bridging, and native Swift on Android, consider reading our Native Swift Tech Preview documentation.

You may also be interested in the nascent swift-java project, which is designed to facilitate communication between server-side Swift and Java libraries. While that is a very different environment than Android apps interacting with modern Kotlin APIs, they do overlap, and you might find swift-java's bridging approach useful. We anticipate that as it matures, this bridge and Skip’s native bridging will begin to align more closely in their techniques and implementation details.

Additional posts in the native Swift on Android series:

Many cross-platform solutions allow you to share code, but they typically come with serious downsides:

  • Performance issues from the use of interpreters and/or complex runtimes (Javascript)
  • High memory watermarks and unpredictable hitches caused by garbage collection (Javascript, Kotlin)
  • Lack of transparent integration with SwiftUI and/or Compose state tracking (C/C++)
  • Portability and memory safety concerns (C/C++)

Swift exhibits none of these problems. Its safety, efficiency, and expressiveness make it an ideal choice for cross-platform development. Swift is already a first-class citizen on Apple platforms, and Skip’s native tooling and technology ensures seamless integration with Android and Compose as well.

Whether you’re creating a single dual-platform app like we did in Part 1, separate iOS and Android apps with a shared model layer and bespoke interfaces like we did in this article, or anything in between, sharing code with Swift can save you significant time and effort when writing your app. More important than the up front savings, though, is the savings over time. A shared Swift codebase will eliminate endless hours of repeated bug fixes, enhancements, team coordination, and general maintenance over the life of your software.

December Skip Newsletter

Skip December Newsletter

Welcome to the December edition of the Skip.tools newsletter! This month we will showcase some of the improvements and advancements we've made to the Skip platform, along with some current events and a peek at our upcoming roadmap.

Skip and Compiled Swift on Android

The big news this month is the release of the Skip's native compiled Swift technology preview! Now you are not limited to just using transpiled packages in Skip apps: you can also embed fully native Swift using our Android toolchain and transparent bridge generation. The SkipFuse framework enables you to move seamlessly between native Swift and your transpiled Jetpack Compose user interface. This unlocks the entire universe of pure-Swift packages for use in your Android app! Read the introductory blog post at https://skip.tools/blog/skip-native-tech-preview/ and then browse the full documentation at https://skip.tools/docs/native/.

Screenshot of native toochain development

New SkipUI Features

SkipUI is the framework that turns your SwiftUI into Jetpack Compose for Android. It enables you to write a single user-interface for both platforms using their platform-native toolkits. SkipUI supports converting nearly all SwiftUI constructs into Compose, but there are sometimes minor deficiencies and quirks that need to be implemented separately for Android. Over the past weeks, we've improved SkipUI with:

  • Support for custom SVG images in asset catalogs
  • Enabling .alert() sheets to containTextField and SecureField views
  • Support for .rotation3DEffect for all views
  • Implementing .interactiveDismissDisabled to conditionally prevent interactive dismissal of sheets

You can always get the latest Skip features and fixes from right within Xcode, by simply clicking File > Packages > Update to Latest Versions. And if you are building from the command-line, swift package update  will do the same thing.

Tip: Customizing with Android-only SwiftUI modifiers

SkipUI supports Android-specific SwiftUI modifiers to customize Material colors, components, and effects. Check out the "Material" section of our SkipUI documentation to see how: https://skip.tools/docs/modules/skip-ui/#material

Skip on Talking Kotlin

We were thrilled to join hosts Sebastian and Márton on the JetBrains Talking Kotlin podcast! The episode was just released, and you can listen to it at https://talkingkotlin.com/going-from-swift-to-kotlin-with-skip/ or watch it at https://www.youtube.com/watch?v=mig81rSWVqM. “Going from Swift to Kotlin with Skip: In a slightly unconventional episode, Sebastian and Márton talk to the founders of Skip, an iOS-to-Android, Swift-to-Kotlin transpiler solution. Marc and Abe have a background working on both Apple platforms and the JVM, and their latest project is a bridge across these two ecosystems.”

Android Police Interview

Another instance of Skip in the press was an interview with the popular Android Police publication, titled: “How the development wall between Android and iOS may soon come down”. You can read the whole thing at: https://www.androidpolice.com/skip-interview

That's all for now

You can follow us on Mastodon at https://mas.to/@skiptools, and join in the Skip discussions at http://community.skip.tools/. The Skip FAQ at https://skip.tools/docs/faq/ is there to answer any questions, and be sure to check out the video tours at https://skip.tools/tour/. And, as always, you can reach out directly to us on our Slack channel at https://skip.tools/slack/.

Happy Skipping!

Native Swift on Android, Part 2: Your First Swift Android App

Native Swift on Android, Part 2: Your First Swift Android App

Swift is Apple’s recommended language for app development, and with good reason. Its safety, efficiency, 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 and Swift on the Playdate highlight developers’ desire to take advantage of Swift on other platforms too. In this series, we explore writing native Swift apps for Android with Skip.

Since its 1.0 release earlier this year, Skip has allowed developers to create cross-platform iOS and Android apps in Swift and SwiftUI by transpiling your Swift to Android’s native Kotlin language. Now, the Skip team is thrilled to give you the ability to use native, compiled Swift for cross-platform development as well.

Part 1 of this series described bringing a native Swift toolchain to Android. Being able to compile Swift on Android, however, is only the first small step towards real-world applications. In this and other installments, we introduce the other pieces necessary to go from printing “Hello World” on the console to shipping real apps on the Play Store:

  • Integration of Swift functionality like logging and networking with the Android operating system.
  • Bridging technology for using Android’s Kotlin/Java API from Swift, and for using Swift API from Kotlin/Java.
  • The ability to power Jetpack Compose and shared SwiftUI user interfaces with native Swift @Observables.
  • Xcode integration and tooling to build and deploy across both iOS and Android.

The best way to learn is often by example. This post introduces you to native Swift apps on Android by exploring the “Hello Swift” app. This is the starter app that Skip generates when you initialize a new project, and it provides a fully-configured launch point for your own cross-platform Swift app development.

Before we can explore the sample, though, we have to install the tools necessary to create it - including Swift for Android!

First, ensure that you are on a macOS 14+ machine with Xcode 16, Android Studio, and Homebrew installed.

Next, open Terminal and type the following commands to install Skip and the native Android toolchain.

brew install skiptools/skip/skip
skip android sdk install

Finally, verify that everything is working with an additional Terminal command:

skip checkup --native

If everything passes successfully, you can now create your first cross-platform native Swift app with the command:

skip init --native-model --open-xcode --appid=com.xyz.HelloSwift hello-swift HelloSwift HelloSwiftModel

That’s a long one! This tells Skip to initialize a new native Swift starter app and open it in Xcode. The app will use the hello-swift project folder, and it will be divided into two modules: a HelloSwift UI layer and a HelloSwiftModel model layer.

When you enter this command, Skip will generate the project, perform some checks, and then the app will open in Xcode. Before running it, though, you must first launch the Android emulator by opening Android Studio.app and selecting the Virtual Device Manager from the ellipsis menu of the Welcome dialog. From there, use the plus toolbar button to Create Device (e.g., “Pixel 6”) and then Launch the emulator.

Screenshot of the Android Studio Device Manager

Finally, select your desired iOS simulator in Xcode, then build and run the “HelloSwift” target.

The first build will take some time to compile the Skip libraries, and you may be prompted with a dialog to affirm that you trust the Skip plugin. Once the build and run action completes, the app will open in the selected iOS simulator, and at the same time the generated Android app will launch in the currently-running Android emulator.

Screenshot of the Hello Swift native app

“Hello Swift” is a very simple CRUD app that contains a list of dated items. You can browse the full source code in Xcode, or online in its GitHub repository. At a high level, the Xcode project embeds a Swift Package Manager package called “HelloSwift”, which contains two targets:

  1. The HelloSwift module contains the SwiftUI for the app’s user interface. It will run natively on iOS and be transpiled by Skip’s “SkipStone” build plugin into Kotlin and Jetpack Compose for Android.
  2. HelloSwiftModel is a pure Swift module that contains an @Observable ViewModel class. It will be compiled natively for both iOS and Android using the Swift toolchain for each platform.

The app allows you to add new items with the plus button, and items can be deleted and re-arranged by swiping and dragging. Tapping an item navigates to a form with editable fields for the various properties: title, date, a favorites toggle, and notes. HelloSwiftModel defines an item as:

public struct Item : Identifiable, Hashable, Codable {
public let id: UUID
public var date: Date
public var favorite: Bool
public var title: String
public var notes: String
}

These items are held by an @Observable ViewModel class:

@Observable public class ViewModel {
public var items: [Item] = loadItems() {
didSet { saveItems() }
}
}

And in the HelloSwift SwiftUI layer, the notes are managed by a SwiftUI List within a NavigationStack:

public struct ContentView: View {
@State var viewModel = ViewModel()
public var body: some View {
NavigationStack {
List {
ForEach(viewModel.items) { item in
NavigationLink(value: item) {
Label {
Text(item.itemTitle)
} icon: {
if item.favorite {
Image(systemName: "star.fill")
.foregroundStyle(.yellow)
}
}
}
}
.onDelete { offsets in
viewModel.items.remove(atOffsets: offsets)
}
.onMove { fromOffsets, toOffset in
viewModel.items.move(fromOffsets: fromOffsets, toOffset: toOffset)
}
}
.navigationTitle(Text("\(viewModel.items.count) Items"))
.navigationDestination(for: Item.self) { item in
ItemView(item: item, viewModel: $viewModel)
.navigationTitle(item.itemTitle)
}
.toolbar {
ToolbarItemGroup {
Button {
withAnimation {
viewModel.items.insert(Item(), at: 0)
}
} label: {
Label("Add", systemImage: "plus")
}
}
}
}
}
}

On iOS, both the HelloSwift and HelloSwiftModel targets are native Swift, so communication between the two layers is seamlessly handled by the Swift dependency. On Android, however, recall that the HelloSwift module’s SwiftUI is transpiled to Kotlin. That Kotlin needs to be able to interact with the HelloSwiftModel module’s compiled Swift, which involves bridging the two languages and runtimes.

Skip’s bridging solution is called “SkipFuse”. Using the SkipStone build plugin, SkipFuse automatically generates bridging code that enables transparent communication between the two layers. This is modeled in the following diagram, which illustrates how the two modules are combined into final iOS and Android app packages:

Skip Native Diagram {: .diagram-vector }

The details of Skip’s bridging are discussed in the documentation. To summarize, the bridging system parses the public types, properties, and functions of your Swift module and exposes them to the transpiled Kotlin layer of your user interface. It supports the Observation framework, so you can use @Observable classes to manage application state in a way that is tracked by your UI, ensuring that your data and user interface are always in sync.

The following screenshot shows a split view between the HelloSwift module’s ContentView.swift and the HelloSwiftModel module’s ViewModel.swift. It demonstrates how the user interface layer communicates with the model layer in exactly the same way on both iOS and Android, although the latter is crossing a language boundary:

Screenshot of the Hello Swift native app

Skip integrates with the Xcode and Swift Package Manager build systems using the SkipStone Xcode plugin. This plugin transpiles your non-native modules from Swift to Kotlin, and it generates the needed bridging code for communication between your native Swift modules and Kotlin or Java.

The skip init command you used to create the “Hello Swift” app also adds a build script to the generated Xcode project. This build script launches Android’s Gradle build tool to compile and package the plugin’s output into an Android APK. When your project has native modules, this includes compiling the native code using the Android toolchain described in Part 1.

But how does Skip know which modules to transpile and which are native? Every Skip module must include a Skip/skip.yml configuration file in its source folder. Here is the configuration for a native Swift module whose public API is bridged to Kotlin:

skip:
mode: 'native'
bridging: true

Once you have specified that a module is bridging, the entire process is automatic. You can iterate on both your native and transpiled code and re-launch the app, and the bridging code will be updated without needing to run an external tool or perform any other manual process.

Now that you’ve created your first native Swift Android app, what’s next? Well, remember that this is just a starter app designed to get you up and running. It is meant to be torn apart and modified, so feel free to experiment by changing the model and UI in Xcode!

If you’d like to learn much more about SkipFuse, bridging, and native Swift on Android, consider reading our Native Swift Tech Preview documentation. Many of the topics it covers are the subjects of additional posts in this series. For example, while we saw “Hello Swift” bridge its native Swift model layer to its transpiled Kotlin user interface, we haven’t discussed bridging Kotlin API for use by native Swift at all. The documentation covers this in depth.

You may also be interested in the nascent swift-java project, which is designed to facilitate communication between server-side Swift and Java libraries. While that is a very different environment than Android apps interacting with modern Kotlin APIs, they do overlap, and you might find swift-java's bridging approach useful. We anticipate that as it matures, this bridge and Skip’s native bridging will begin to align more closely in their techniques and implementation details.

Additional posts in the native Swift on Android series:

Swift’s safety, efficiency, and expressiveness make it an excellent development language, and its use across platforms is spreading. This series focuses on sharing native Swift code between iOS and Android with Skip. Part 1 introduced the native Swift toolchain for Android. Now in Part 2, you’ve created your first cross-platform Swift app. There is a lot of interesting territory that we haven’t yet explored, so check out Part 3 and beyond!

Native Swift on Android, Part 1: Setup, Compiling, Running, and Testing

You may already be familiar with Skip as a tool for bringing your Swift iOS apps to Android. Skip takes a novel transpilation approach, where we integrate with the Xcode build system to convert your Swift code into Kotlin. This allows us to create an Android library for every build of your Swift package, or to launch an Android version of your SwiftUI app on every Xcode Run.

We’ve discussed the advantages of a transpilation-based strategy in the past. But despite the fact that Android is a Java/Kotlin-oriented platform, there are also significant benefits to compiled code. Skip has featured support for integrating with C code on both Android and iOS for a long time. It only makes sense that our transpiled Swift code should also integrate with compiled Swift code.

Swift Android Logo {: style=“text-align: center; width: 200px; margin: auto;”}

And so we are excited to announce the first technology preview of a native Swift toolchain and driver for Android! This toolset enables developers to build and run Swift executables and test cases on a connected Android device or emulator.

On a macOS development machine with Xcode and Homebrew installed, you can install the Swift 6.0 Android toolchain by opening a terminal and running:

brew install skiptools/skip/swift-android-toolchain@6.0

This will download the Swift Android SDK, along with all the dependencies it needs to build, run, and test Swift packages on Android.

If you’re an existing Skip user, make sure to also update your skip copy to version 1.1.1+:

skip upgrade

Unless you have an Android device handy, you will need to install the Android emulator in order to run executables and test cases in a simulated Android environment. The simplest way to do this is to download and install Android Studio, then launch it and open the “Virtual Device Manager” from the “More Actions” (or ellipsis menu) of the “Welcome to Android Studio” dialog. On the resulting “Device Manager” screen, select “Create virtual device”.

Android Emulator Setup 1: Welcome Screen Android Emulator Setup 2: Device Manager

On the “Select Hardware” screen, select a device (e.g., “Pixel 6”) and then on the “Select a system image” screen select one of the recommended images (e.g., “UpsideDownCake”, a.k.a. API 34), and then on the next screen name the device and select “Finish”. When you return to the “Device Manager” screen, you will see a new device (like “Pixel 6 API 34”), which you can then launch with the triangular play button. A little window titled “Android Emulator” will appear and the operating system will boot.

Android Emulator Setup 3: Select Hardware Android Emulator Setup 4: Select System Image Android Emulator Setup 5: Verify Connfiguration
Android Emulator Setup 6: Device Manager Android Emulator Setup 6: Running Emulator

Running Swift “Hello World” on Android

Section titled “Running Swift “Hello World” on Android”

Now that you have everything set up and have launched an Android emulator (or connected a physical Android device with developer mode enabled), it’s time to run some Swift!

Open a terminal and create a new Swift command-line executable called “HelloSwift”:

% mkdir HelloSwift
% cd HelloSwift
% swift package init --type=executable
Creating executable package: HelloSwift
Creating Package.swift
Creating Sources/main.swift

Just to make sure it works on macOS, run the program with the standard swift run command:

% swift run HelloSwift
Building for debugging...
Build of product 'HelloSwift' complete! (1.80s)
Hello, world!

And now, we will build and run it on the Android emulator (or device) using the Swift Android driver, which we include as part of the skip tool that was installed along with the toolchain:

% skip android run HelloSwift
Building for debugging...
Build complete! (10.90s)
[✓] Check Swift Package (0.68s)
[✓] Connecting to Android (0.05s)
[✓] Copying executable files (0.25s)
Hello, world!

Viola! There’s Swift running on Android. And just to prove to that we are really running on a different host, edit the Sources/main.swift file with your favorite editor (or run xed Sources/main.swift to edit it in Xcode), and add a platform check:

#if os(Android)
print("Hello, Android!")
#elseif os(macOS)
print("Hello, macOS!")
#else
print("Hello, someone other platform…")
#endif

Then run it on both macOS and Android:

% swift run HelloSwift
Building for debugging...
Build of product 'HelloSwift' complete! (0.47s)
Hello, macOS!
% skip android run HelloSwift
Building for debugging...
Build complete! (0.89s)
[✓] Check Swift Package (0.23s)
[✓] Connecting to Android (0.04s)
[✓] Copying executable files (0.23s)
Hello, Android!

Command-line tools are fun, but to really exercise Swift on Android, we want to be able to run test suites. This is how developers interested in creating cross-platform frameworks will be able to check for – and resolve – issues with their Swift code arising from platform differences.

Fortunately the skip android driver includes not just the run command, but also the test command, which will connect to the Android emulator/device and run through an XCTest test suite in the same way as swift test does for macOS.

To demonstrate, we can run the test suite for Apple’s swift-algorithms package, to make sure it runs correctly on Android:

% git clone https://github.com/apple/swift-algorithms.git
Cloning into 'swift-algorithms'...
Resolving deltas: 100% (1054/1054), done.
% cd swift-algorithms
% skip android test
Fetching https://github.com/apple/swift-numerics.git from cache
Fetched https://github.com/apple/swift-numerics.git from cache (0.87s)
Computing version for https://github.com/apple/swift-numerics.git
Computed https://github.com/apple/swift-numerics.git at 1.0.2 (0.57s)
Creating working copy for https://github.com/apple/swift-numerics.git
Working copy of https://github.com/apple/swift-numerics.git resolved at 1.0.2
Building for debugging...
[92/93] Linking swift-algorithmsPackageTests.xctest
Build complete! (25.91s)
[✓] Check Swift Package (0.74s)
[✓] Connecting to Android (0.06s)
[✓] Copying test files (0.27s)
Test Suite 'All tests' started at 2024-09-10 20:24:17.770
Test Suite 'swift-algorithms-C7A0585A-0DC2-4937-869A-8FD5E482398C.xctest' started at 2024-09-10 20:24:17.776
Test Suite 'AdjacentPairsTests' started at 2024-09-10 20:24:17.776
Test Case 'AdjacentPairsTests.testEmptySequence' started at 2024-09-10 20:24:17.776
Test Case 'AdjacentPairsTests.testEmptySequence' passed (0.001 seconds)
Test Case 'WindowsTests.testWindowsSecondAndLast' started at 2024-09-10 20:24:20.480
Test Case 'WindowsTests.testWindowsSecondAndLast' passed (0.0 seconds)
Test Suite 'WindowsTests' passed at 2024-09-10 20:24:20.480
Executed 8 tests, with 0 failures (0 unexpected) in 0.004 (0.004) seconds
Test Suite 'swift-algorithms-C7A0585A-0DC2-4937-869A-8FD5E482398C.xctest' passed at 2024-09-10 20:24:20.480
Executed 212 tests, with 0 failures (0 unexpected) in 2.702 (2.702) seconds
Test Suite 'All tests' passed at 2024-09-10 20:24:20.480
Executed 212 tests, with 0 failures (0 unexpected) in 2.702 (2.702) seconds

Everything passes. Hooray!

Not every package’s tests will pass so easily: Android is based on Linux – unlike the Darwin/BSD heritage of macOS and iOS – so there may be assumptions your code makes for Darwin that don’t hold true on Linux. Running through a comprehensive test suite is the best way to begin isolating, and then addressing, these platform differences.

Command line executables and unit tests are all well and good, but “Hello World” is not an app. To create an actual Android app, with access to device capabilities and a graphical user interface, you need to work with the Android SDK, which is written in Java and Kotlin. And you need to package and distribute the app in Android’s own idiomatic way, with self-contained libraries embedded in the application’s assembly.

This is where integration with Skip’s broader ecosystem comes into play. Additional installments of this series explore Skip’s system for transparently bridging compiled Swift to Java, Kotlin, and transpiled Swift - including Skip’s existing SwiftUI support for Android. This allows the best of all worlds: transpiled Swift to talk to Android libraries, SwiftUI on top of Jetpack Compose, and business logic and algorithms implemented in compiled Swift!

Screenshot

Additional posts in the native Swift on Android series:

The Swift toolchain for Android is the culmination of many years of community effort, in which we (the Skip team) have played only a very small part.

Even before Swift was made open-source, people have been tinkering with getting it running on Android, starting with Romain Goyet’s “Running Swift code on Android” attempts in 2015, which got some basic Swift compiling and running on an Android device. A more practical example came with Geordie J’s “How we put an app in the Android Play Store using Swift” in 2016, where Swift was used in an actual shipping Android app. Then in 2018, Readdle published “Swift for Android: Our Experience and Tools” on integrating Swift into their Spark app for Android. These articles provide valuable technical insight into the mechanics and complexities involved with cross-compiling Swift for a new platform.

In more recent years, the Swift community has had various collaborative and independent endeavors to develop a usable Swift-on-Android toolchain. Some of the most prominent contributors on GitHub are @finagolfin, @vgorloff, @andriydruk, @compnerd, and @hyp. Our work merely builds atop of their tireless efforts, and we expect to continue collaborating with them in the hopes that Android eventually becomes a fully-supported platform for the Swift language.

Looking towards the future, we are eager for the final release of Swift 6.0, which will enable us to publish a toolchain that supports all the great new concurrency features, as well as the Swift Foundation reimplementation of the Foundation C/Objective-C libraries, which will give us the the ability to provide better integration between Foundation idioms (bundles, resources, user defaults, notifications, logging, etc.) and the standard Android patterns. A toolchain is only the first step in making native Swift a viable tool for building high-quality Android apps, but it is an essential component that we are very excited to be adding to the Skip ecosystem.

August Skip Newsletter

Welcome to the August edition of the Skip.tools newsletter! This month we will showcase some of the improvements and advancements we've made to the Skip platform, along with some current events and a peek at our upcoming roadmap.

Skip 1.0

The big news this month was the launch of Skip 1.0! After over a year in development, Skip is finally ready for general production use. The launch has made a big splash, even being featured on the front page of Hacker News. There has never been a better time to start a new project with Skip and bring your app to the entire mobile marketplace!

New FREE Indie Pricing Tier

As part of the general availability of Skip, we are also delighted to announce the new Skip Indie tier, which enables solo developers to use Skip to build their dual-platform projects for free.

Markdown Support

On a technical front, a long-requested feature for SkipUI has been to support SwiftUI's automatic markdown support for Text elements. Well, now it's here! Styling text with simple markdown elements has never been simpler.

Reminder: Skip Showcase on the Stores

The Skip Showcase app (https://skip.tools/docs/samples/skipapp-showcase/) has long been our go-to for providing a side-by-side comparison of SwiftUI components with the Jetpack Compose equivalents that SkipUI provides. Browsing thought these components simultaneously on an iPhone and Android device gives a good sense Skip's capabilities and power, and is a great way to demonstrate Skip's benefits to project managers and stakeholders before breaking ground on a new project.

In order to make it even easier to get this handy app on your devices, we've published the Skip Showcase app to both the Apple App Store (https://apps.apple.com/us/app/skip-showcase/id6474885022) as well as the Google Play Store (https://play.google.com/store/apps/details?id=org.appfair.app.Showcase). This enables you to quickly grab a demo app that highlights Skip's power, and feel for yourself the benefit of using a genuinely native app on both platforms. Download it today and see for yourself what Skip can do!

New Skip Showreel Video

We've published a new 3-minute video summarizing Skip's capabilities. This is a great video to share with your colleagues and management to highlight some of the benefits of using Skip to bring your app to the entire matketplace. Check it out at https://skip.tools/tour/skip-showreel/

Video poster image

That's all for now!

You can follow us on Mastodon at https://mas.to/@skiptools, and join in the Skip discussions at http://community.skip.tools/. The Skip FAQ at https://skip.tools/docs/faq/ is there to answer any questions, and be sure to check out the video tours at https://skip.tools/tour/. And, as always, you can reach out directly to us on our Slack channel at https://skip.tools/slack/.

Happy Skipping!

Skip 1.0 Release

Screenshot

We’re thrilled to announce the release of Skip 1.0!

Skip brings Swift app development to Android. Share Swift business logic, or write entire cross-platform apps in SwiftUI.

Skip is the only tool that enables you to develop genuinely native apps for both major mobile platforms with a single codebase. Under the hood, it uses the vendor-recommended technologies on each OS: Swift and SwiftUI on iOS, Kotlin and Compose on Android. So your apps don’t just “look native”, they are native, with no additional resource-hogging runtime and no uncanny-valley UI replicas.

Skip also gives you complete access to platform libraries. Directly call any Swift or Objective C API on iOS, and any Kotlin or Java API on Android - no complex bridging required!

Skip has been in development for over a year. It has an enthusiastic community of users developing a wide range of apps and continually improving Skip’s ecosystem of cross-platform open source libraries.

Scrumskipper: Running Apple's SwiftUI sample app on Android

Scrumdinger is Apple’s canonical SwiftUI iPhone app. In this post, we’ll use Skip to run Scrumdinger as a native Android app as well!

Recording of Scrumdinger app operating on iOS and Android simultaneously

Honed and updated over the years, Apple’s Scrumdinger tutorial is an hours-long step-by-step guide to building a complete, modern SwiftUI app. It exercises both built-in UI components and custom drawing, and it takes advantage of Swift language features like Codable for persistence. As its rather unique name implies, the Scrumdinger app allows users to create and manage agile programming scrums on their phones.

This blog post begins where Apple’s tutorial ends. We’ll start with the final Scrumdinger source code and walk you through the process of bringing the full app to Android using Skip. You’ll learn the general steps involved in bringing an existing app to Android, and you’ll become familiar with the types of issues you may encounter and how to overcome them. Let’s get started!

You can get the full Scrumdinger source code from the last page of Apple’s SwiftUI tutorial. Here’s a direct link to the zipped Xcode project:

https://docs-assets.developer.apple.com/published/9d1c4a1d2dcd046ee8e30ad15f20f6f3/TranscribingSpeechToText.zip

Download and expand the zip file. Assuming you have the latest Xcode installed, you can run the iPhone app by opening TranscribingSpeechToText/Complete/Scrumdinger.xcodeproj. The first time you attempt to open it, you may need to confirm that you trust the download. Once Xcode has loaded the project, select the iOS Simulator you’d like to use and hit the Run button!

Screenshot of the iOS app's scrum list Screenshot of the iOS app's scrum detail Screenshot of the iOS app's meeting view

Play around with the app - this is what we’re going to bring to Android. First, however, we need to install Skip.

Skip is a tool for building fully native iOS and Android apps from a single Swift and SwiftUI codebase. It works by transpiling your Swift into Android’s Kotlin development language and adapting your SwiftUI to Android’s native Jetpack Compose UI framework.

Skip’s Android version of Scrumdinger won’t be pixel-identical to the iOS version, and it shouldn’t be. Rather, we believe in using the native UI framework and controls on each platform. This gives the best possible user experience, avoiding the uncanny-valley feel of non-native solutions.

Follow the Getting Started guide to install Skip and your Android environment, including Android Studio. Next, launch Android Studio and open the Virtual Device Manager from the ellipsis menu of the Welcome dialog. From there, Create Device (e.g., “Pixel 6”) and then start the Emulator. Skip needs a connected Android device or Emulator to run the Android version of your app.

Screenshot of Android Studio Device Manager

Now we’re ready to turn Scrumdinger into a dual-platform Skip app.

It isn’t too hard to update an existing Swift Package Manager package to use Skip. Updating an existing app, however, is a different story. Building for Android requires a specific folder structure and xcodeproj configuration. We recommend creating a new Skip Xcode project, then importing the old project’s code and resources.

Enter the following command in Terminal to initialize Scrumskipper, your dual-platform version of the Scrumdinger app.

Terminal window
skip init --open-xcode --appid=com.xyz.Scrumskipper scrum-skipper Scrumskipper

This will create a template SwiftUI app and open it in Xcode. Let’s run the template as-is to make sure it’s working: select your desired iOS Simulator in Xcode, and hit the Run button. If you just installed or updated Skip, you may have to trust the Skip plugin:

Xcode's Trust Plugin dialog

If all goes well, you should see something like the following:

Screenshot of template app running in iOS Simulator and Android Emulator

Great! Next, copy Scrumdinger’s source code to Scrumskipper:

  1. Drag the Scrumdinger/Models and Scrumdinger/Views folders from Scrumdinger’s Xcode window into the Scrumskipper/Sources/Scrumskipper/ folder in Scrumskipper’s window.

    Copy source folders from Scrumdinger to Scrumskipper
  2. Replace Scrumskipper’s ContentView body with the content of Scrumdinger’s primary WindowGroup. Scrumskipper’s ContentView should now look like this:

import SwiftUI
public struct ContentView: View {
@StateObject private var store = ScrumStore()
@State private var errorWrapper: ErrorWrapper?
public init() {
}
public var body: some View {
ScrumsView(scrums: $store.scrums) {
Task {
do {
try await store.save(scrums: store.scrums)
} catch {
errorWrapper = ErrorWrapper(error: error,
guidance: "Try again later.")
}
}
}
.task {
do {
try await store.load()
} catch {
errorWrapper = ErrorWrapper(error: error,
guidance: "Scrumdinger will load sample data and continue.")
}
}
.sheet(item: $errorWrapper) {
store.scrums = DailyScrum.sampleData
} content: { wrapper in
ErrorView(errorWrapper: wrapper)
}
}
}
#Preview {
ContentView()
}

That’s it! You’ve now created Scrumskipper, a dual-platform app with all of Scrumdinger’s source code.

It’s the moment of truth: hit that Xcode Run button!

Almost immediately, you’ll get an API unavailable error like this one:

Xcode API unavailable error from Skip

This is our first hint that migrating an existing iOS codebase to Android is not trivial, even with Skip. Starting a new app with Skip can be a lot of fun, because it’s relatively easy to avoid problematic patterns and APIs, and you can tackle any issues one at a time as they appear. But when you take on an existing codebase, you get hit with everything at once. Even if Skip perfectly translates 95+% of your original Swift source and API calls - code that was certainly never intended to be cross-platform - that can leave dozens or even hundreds of errors to deal with!

It’s important to remember, though, that while fixing that remaining 5% can be a slog, it is still 20 times less work than a 100% Android rewrite! And once you’ve worked through the errors, you’ll have a wonderfully maintainable, unified Swift and SwiftUI codebase moving forward. So let’s roll up our sleeves and get started, beginning with the error above.

The pictured error message says that the Color(_ name:) constructor isn’t available in Skip. Each of Skip’s major frameworks includes a listing you can consult of the API that is supported on Android. These listings are constantly expanding as we port additional functionality. For example, here is the table of supported SwiftUI.

When an API is unsupported, that does not mean you can’t use it in your app! Skip never forces you to compromise your iOS app. Rather, it means that you have to find a solution for your Android version. That could mean contributing an implementation for the missing API, but more often you’ll just want to take a different Android code path. To keep your iOS code intact but create an alternate Android code path, use #if SKIP (or #if !SKIP) compiler directives. The Skip documentation covers compiler directives and other platform customization techniques in detail. Let’s update the problematic code in Theme.swift to use named colors on iOS, but fall back to a constant color on Android until we implement a solution:

Change:

var mainColor: Color {
Color(rawValue)
}

To:

var mainColor: Color {
#if !SKIP
Color(rawValue)
#else
// TODO
Color.yellow
#endif
}

This technique works for SwiftUI modifiers as well. For example, while Skip is able to translate many of SwiftUI’s accessibility modifiers for Android (in fact Skip’s native-UI approach excels in accessibility), it doesn’t have a translation for the .accessibilityElement(children:) modifier. So, update from this:

HStack {
Label("Length", systemImage: "clock")
Spacer()
Text("\(scrum.lengthInMinutes) minutes")
}
.accessibilityElement(children: .combine)

To this:

HStack {
Label("Length", systemImage: "clock")
Spacer()
Text("\(scrum.lengthInMinutes) minutes")
}
#if !SKIP
.accessibilityElement(children: .combine)
#endif

The iOS version of the app is unaffected, and the Android build can proceed without the unsupported modifier.

Getting Scrumskipper to successfully build as an iOS and Android app is a repetition of the process we began above:

  1. Hit the Run button.
  2. View the next batch of Xcode errors from the transpiler or the Kotlin compiler.
  3. Use compiler directives to exclude the source causing the error from the Android build.

If you see this process through as we did, you’ll end up using #if SKIP and #if !SKIP approximately 25 times in the ~1,500 lines of Scrumdinger’s Swift and SwiftUI source. In addition to the aforementioned Color(_ name:) and .accessibilityElement(children:) APIs, here is a full accounting of Scrumdinger code that causes errors:

  • The Speech and AVFoundation frameworks used in Scrumdinger are not supported. While Skip does have a minimal AVFoundation implementation for Android, it is not complete as of this writing. These are examples of cases where you would likely use Skip’s dependency support to integrate Android-specific libraries for the missing functionality.
  • Timer.tolerance is not supported.
  • .ultraThinMaterial is not supported.
  • ListFormatter is not supported. We replaced ListFormatter.localizedString(byJoining: attendees.map { $0.name }) with attendees.map { $0.name }.joined(separator: ", ").
  • SwiftUI’s ProgressViewStyle is not yet supported.
  • SwiftUI’s LabelStyle is not yet supported.
  • The Theme enum’s name property causes an error because all Kotlin enums inherit a built-in, non-overridable name property already. We changed the property to themeName.

You can view the final result in the source of our skipapp-scrumskipper sample app.

After using compiler directives to work around these errors, we have liftoff! Scrumskipper launches on both the iOS Simulator and Android Emulator. Clearly, however, it needs additional work.

Screenshot of the Android app's scrum list Screenshot of the Android app's scrum detail Screenshot of the Android app's meeting view

As you explore the app, you’ll find many things that are missing or broken. Fortunately, it turns out that the problems are all easily fixed. View our sample app to see the completed code. We give a brief description of each issue below.

Copying over Scrumdinger’s source code wasn’t quite enough. We forgot to copy its ding.wav resource and more importantly, the Info.plist keys that give the app permission to use the microphone and speech recognition. Without these keys, the iOS app will crash when you attempt to start a meeting.

Scrumdinger uses SF Symbols for all of its in-app images. Android obviously doesn’t include these out of the box, but Skip allows you to easily add symbols to your app. Just place vector images with the desired symbol names in your Module asset catalog, as described here. For the purposes of this demo, we exported the symbols from Apple’s SF Symbols app.

Exporting a SF Symbol Copying SF Symbols to Scrumskipper

We made two additional tweaks to MeetingView. First, the custom MeetingTimerView was not rendering at all on Android. Layout issues like this are rare, but they do occur. Second, we didn’t want the Android navigation bar background to be visible. We added a couple of #if SKIP blocks to the view body to correct these issues:

var body: some View {
ZStack {
...
VStack {
MeetingHeaderView(...)
MeetingTimerView(...)
#if SKIP
.frame(maxWidth: .infinity, maxHeight: .infinity)
#endif
MeetingFooterView(...)
}
}
...
#if SKIP
.toolbarBackground(.hidden, for: .navigationBar)
#endif
}

Scrums were not being saved and restored in the Android version of the app. It turns out that Scrumdinger saved its state on transition to ScenePhase.inactive, but Android doesn’t use this phase! Android apps transition directly from active to background and back. The following simple change fixed the issue:

struct ScrumsView: View {
@Environment(\.scenePhase) private var scenePhase
...
var body: some View {
NavigationStack {
...
}
.onChange(of: scenePhase) { phase in
#if !SKIP
if phase == .inactive { saveAction() }
#else
if phase == .background { saveAction() }
#endif
}
}
}

Remember how we were going to circle back to the Theme enum’s named colors on Android? We decided to forgo the asset catalog colors altogether and just define each color programmatically with RGB values.

The completed native Android app actually looks a lot like its iOS counterpart:

Screenshot of the Android app's scrum list Screenshot of the Android app's scrum detail Screenshot of the Android app's edit view Screenshot of the Android app's meeting view

Take a moment to reflect on how amazing it is that Apple’s canonical SwiftUI iPhone sample now runs as a fully native Android app. Though Skip excels at new development and the process for bringing Scrumdinger’s existing code to Android wasn’t trivial, it was still an order of magnitude faster than a re-write and did not risk regressions to the iOS version. Moreover, future maintenance and improvements will be extremely efficient thanks to having a single shared Swift and SwiftUI codebase.

The Android version doesn’t yet have all the features of the iOS one, but additional Android functionality can be added over time. Skip never forces you to compromise your iOS app, provides a fast path to a native Android version using your existing code, and has excellent integration abilities to enhance and specialize your Android app when the effort warrants it.

Skip and Kotlin Multiplatform

  • Table of contents {:toc}

Kotlin Multiplatform (KMP) is a technology that enables Kotlin to be compiled natively and used in non-Java environments. Google recommends using KMP for sharing business logic between Android and iOS platforms1.

In many ways, Skip and KMP are inverses of each other, in that:

  • Skip brings your Swift/iOS codebase to Android.
  • KMP brings your Kotlin/Android codebase to iOS.

The mechanics powering these transformations are different – Skip uses source transpilation to convert Swift into idiomatic Kotlin, whereas KMP compiles Kotlin into native code that presents an Objective-C interface – but the high-level benefits are the same: you can maintain a single codebase for both your iOS and Android app.

Skip or KMP {: style=“text-align: center; width: 50%; margin: auto;”}

We think that Skip is the right way to tackle the challenge of creating genuinely native dual-platform apps. Skip gives you an uncompromised iOS-first development approach: your code is used as-is on iOS devices, with zero bridging and no added runtime or garbage collector2. Our SkipUI adaptor framework – which takes your SwiftUI and converts it into Jetpack Compose – allows you to create genuinely native user interfaces for both platforms. And while the Compose Multiplatform3 project adds cross-platform Compose support to KMP, it eschews native components on iOS by default. It utilizes a Flutter-like strategy instead, painting interface elements onto a Skia canvas. This can result in a sub-par experience for iOS users in terms of aesthetics, performance, accessibility, and feel, not to mention limitations on native component integration (something Skip excels at). We believe that a premium, no-compromises user experience requires embracing the platform’s native UI toolkit.

If we put the UI layer aside, however, using KMP for logic and model code does have some great benefits. With KMP, you can target not just Android and iOS, but also the web, desktop, and server-side environments, whereas Skip is focused squarely on mobile app development. You can also write and build KMP code on a variety of platforms: macOS, Windows, and Linux. Finally, some organizations might already be heavily invested in the Kotlin/Java ecosystem.

Skip and KMP {: style=“text-align: center; width: 50%; margin: auto;”}

And so it may be the case that you have business logic in one or more KMP modules that you want to use in a cross-platform Android and iOS app. The trend among organizations that have adopted KMP has been to build separate native apps for each platform – using Jetpack Compose (or Views) on Android and SwiftUI (or UIKit) on iOS – and then import their KMP business logic module into those apps. This is much the same as writing two separate apps, but with the benefit that some of the business logic can use a shared codebase.

This happens to be a perfect fit for Skip. With Skip/KMP integration, you can build the UI of your app from a single Swift codebase, and at the same time use the KMP module from both the Swift and (transpiled) Kotlin sides of the app. You get all the benefits of a genuinely native user interface, and can still leverage any existing investment in a shared Kotlin codebase. The remainder of this post will outline the details of integrating and accessing a KMP module from a Skip app project.

When viewed from the Android side, a KMP module is simply a traditional Kotlin/Gradle project dependency: you add it to your build.gradle.kts and import the Kotlin packages in the same way as you use any other Kotlin package. The KMP module has no knowledge of, or dependency on, any Skip libraries or tools.

The iOS side is a bit more involved: the KMP project must be compiled and exported as a native library4 that can be imported into the iOS project. This is done by compiling the KMP project to native code and then exporting it to an xcframework, which is a multi-platform binary framework bundle that is supported by Xcode and SwiftPM.

The resulting project and dependency layout will look like this:

Skip KMP Diagram {: .diagram-vector }

Adding a KMP dependency to a Skip Framework

Section titled “Adding a KMP dependency to a Skip Framework”

In the Package.swift file for the skip-kmp-sample library, we have the Skip-enabled “SkipKMPSample” target with a dependency on a binary target specifying the location and checksum of the compiled xcframework. This enables us to access the Objective-C compiled interface for the KMP library, which is in the separate kmp-library-sample repository containing the Kotlin and Gradle project that builds the library.

5.9
import PackageDescription
let package = Package(
name: "skip-kmp-sample",
defaultLocalization: "en",
platforms: [.iOS(.v16), .macOS(.v13), .tvOS(.v16), .watchOS(.v9), .macCatalyst(.v16)],
products: [
.library(name: "SkipKMPSample", targets: ["SkipKMPSample"]),
],
dependencies: [
.package(url: "https://github.com/skiptools/skip.git", from: "0.8.55"),
.package(url: "https://github.com/skiptools/skip-foundation.git", from: "0.6.12")
],
targets: [
.target(name: "SkipKMPSample", dependencies: [
.product(name: "SkipFoundation", package: "skip-foundation"),
"MultiPlatformLibrary"
], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
.testTarget(name: "SkipKMPSampleTests", dependencies: [
"SkipKMPSample",
.product(name: "SkipTest", package: "skip")
], resources: [.process("Resources")], plugins: [.plugin(name: "skipstone", package: "skip")]),
.binaryTarget(name: "MultiPlatformLibrary",
url: "https://github.com/skiptools/kmp-library-sample/releases/download/1.0.4 /MultiPlatformLibrary.xcframework.zip",
checksum: "65e97edcdeadade0f10ef0253d0200bce0009fe11f9826dc11ad6d56b6436369")
]
)

For the transpiled Kotlin side of the Skip framework, we add a Gradle source dependency5 to that same repository. This is accomplished by using the module’s skip.yml file to add the dependency on the same tagged version as the published xcframework:

settings:
contents:
- block: 'sourceControl'
contents:
- block: 'gitRepository(java.net.URI.create("https://github.com/skiptools/kmp-library-sample.git"))'
contents:
- 'producesModule("kmp-library-sample:multi-platform-library")'
build:
contents:
- block: 'dependencies'
contents:
- 'implementation("kmp-library-sample:multi-platform-library:1.0.4")'

The result is that the Kotlin side of the Skip project will depend on the Kotlin library, and the Swift side of the Skip project will access the natively-compiled xcframework of the KMP library via its exported Objective-C interface.

Using KMP code from a Skip app is generally the same as using KMP from any other app. As already mentioned, on the Android side, the KMP module is included directly as Kotlin code, and compiled to JVM bytecode along with the rest of your app. On the iOS side, the code is compiled natively to each of the supported architectures (ARM iOS, ARM/X86 iOS Simulator, and ARM/X86 macOS) and bundled into an xcframework. As part of this packaging the KMP compiler generates an Objective-C interface to the native code. This interface can then be used from your Swift through the automatic Objective-C bridging provided by the Swift language.

A simple example can be illustrated using the following Kotlin class:

class SampleClass(var stringField: String, var intField: Int, val doubleField: Double) {
fun addNumbers() : Double {
return intField + doubleField
}
suspend fun asyncFunction(duration: Long) {
delay(duration)
}
@Throws(Exception::class)
fun throwingFunction() {
throw Exception("This function always throws")
}
}

The Objective-C header created by the Kotlin/Native compiler for this class will look like this:

__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("SampleClass")))
@interface MPLSampleClass : MPLBase
@property (readonly) double doubleField __attribute__((swift_name("doubleField")));
@property int32_t intField __attribute__((swift_name("intField")));
@property NSString *stringField __attribute__((swift_name("stringField")));
- (instancetype)initWithStringField:(NSString *)stringField intField:(int32_t)intField doubleField:(double)doubleField __attribute__((swift_name("init(stringField:intField:doubleField:)"))) __attribute__((objc_designated_initializer));
- (double)addNumbers __attribute__((swift_name("addNumbers()")));
/**
* @note This method converts instances of CancellationException to errors.
* Other uncaught Kotlin exceptions are fatal.
*/
- (void)asyncFunctionDuration:(int64_t)duration completionHandler:(void (^)(NSError * _Nullable))completionHandler __attribute__((swift_name("asyncFunction(duration:completionHandler:)")));
/**
* @note This method converts instances of Exception to errors.
* Other uncaught Kotlin exceptions are fatal.
*/
- (BOOL)throwingFunctionAndReturnError:(NSError * _Nullable * _Nullable)error __attribute__((swift_name("throwingFunction()")));
@end

When viewed as Swift, the Objective-C interface will be represented as:

public class SampleClass: NSObject {
public var stringField: String { get set }
public var intField: Int32 { get set }
public var doubleField: Double { get }
public init(stringField: String, intField: Int32, doubleField: Double)
public func addNumbers() -> Double
public func asyncFunction(duration: Int64) async throws
public func throwingFunction() throws
}

This Swift interface derived from the generated Objective-C is idiomatic, much in the same way that Skip’s transpiled code is idiomatic Kotlin. This results in code that can be used from both sides of your dual-platform Swift project using the same interface. For example, this Swift code will work on both sides of a Skip project, where the Swift code instantiates the Objective-C class, and the transpiled Kotlin code instantiates the Java class.

func performAdd() -> Double {
let instance = SampleClass(stringField: "XYZ", intField: Int32(123), doubleField: 12.23)
return instance.addNumbers()
}

The type mapping section of the Interoperability with Swift/Objective-C documentation goes over the automatic conversion of various Kotlin types into their closest Objective-C equivalents. This, in turn, will affect how the Swift types are represented.

These type mappings will typically be the same as the type mappings used by Skip to represent Swift types in Kotlin (such as a Kotlin Short being represented by a Swift Int16), but they don’t always line up exactly. In these cases, there may need to be some manual coercion of types inside an #if SKIP block to get both the Swift and transpiled Kotlin to behave the same.

Kotlin doesn’t have functions that declare that they might throw an exception (like Swift and Java), but if you add the @Throws annotation to a function, the Kotlin/Native compiler will generate Objective-C that accepts a trailing NSError pointer argument, which is, in turn, represented in Swift as a throwing function. For example, the following Kotlin:

@Throws(IllegalArgumentException::class)
public func someThrowingFunction() {
}

will be represented in Swift as:

func someThrowingFunction() throws {
}

In the same way that Skip transforms Swift async functions into Kotlin coroutines, the Kotlin/Native compiler will generate Objective-C with a trailing completionHandler parameter that will be represented in Swift as an async throwing function.

For example, the Kotlin function:

suspend fun someAsyncFunction(argument: String): String {
}

will be represented in Swift as:

func someAsyncFunction(argument: String) async throws {
}

Considering that Skip was not designed with KMP integration in mind – nor vice-versa – it is remarkable how well they work together out of the box. Classes, functions, async, throwable: all work without any special consideration by the Skip transpiler.

That being said, the integration is not perfect. You may encounter some of the following issues:

  • Primitive Boxing: KMP will box primitives when wrapping in collection types like arrays. For example, a Kotlin function that returns an [Int] will be represented in Objective-C as an NSArray<MPLInt *>, where MPLInt is an NSNumber type. And while the automatic Objective-C bridging will handle converting the NSArray into a Swift Array, it will not know enough about MPLInt to convert it into a Swift Int32, so that sort of conversion will need to be handled manually.
  • Static Functions: KMP doesn’t map object functions to Objective-C static functions in the way that Skip assumes, but rather handles a Kotlin object as a singleton instance of the type accessible through a shared property.

More can be read about platform-specific behavior in the Kotlin/Native iOS integration docs.

To experiment with your own Skip/KMP integrations, we recommend starting with our pair of example repositories:

  • kmp-library-sample: a basic KMP project that presents a source Kotlin dependency, and whose releases publish a binary xcframework artifact.
  • skip-kmp-sample: a basic Skip project that depends on the kmp-library-sample’s published xcframework on the Swift side, and has a source dependency on the kmp-library-sample gradle project on the Kotlin side. The test cases in this project utilize Skip’s parity testing to ensure that the tests pass on each supported architecture: macOS and Robolectric for local testing, as well as iOS and Android for connected testing.

These two projects work together to provide a minimal working example of Skip’s KMP integration, and can be used as the basis for further development.

Skip presents an iOS-first, full-stack approach to writing apps for iOS and Android. From the low-level logic layers to the high-level user interface, Skip provides a vertically integrated stack of frameworks that enable the creation of best-in-class apps using the native UI toolkits for both of the dominant mobile platforms.

Kotlin Multiplatform has benefits too: KMP modules can be written and tested on multiple platforms, and they can target platforms beyond mobile. For this reason, KMP can be a compelling option for people who want to share their mobile app code with the web, desktop, or server-side applications.

KMP code fits nicely with Skip projects, because its idiomatic native Objective-C representation means that, for the most part, it can be used seamlessly from both the source Swift and transpiled Kotlin sides of a project. Whether you are creating KMP modules because you are invested in the Kotlin/Java ecosystem, or because you are starting to migrate away from an Android-centric app infrastructure, Skip provides the ideal complement to your existing KMP code. You can have the genuinely native user interface for both platforms that Skip provides, while at the same time utilizing the Kotlin code that you may have built up over time. It is truly the best of both worlds!

  1. “We use Kotlin Multiplatform within Google and recommend using KMP for sharing business logic between Android and iOS platforms.” – https://developer.android.com/kotlin/multiplatform. The mobile-specific form of KMP had been known as KMM, or “Kotlin Multiplatform Mobile”, until they recently deprecated the term.

  2. KMP embeds a garbage collector into its compiled iOS framework. Kotlin/Native’s GC algorithm is a stop-the-world mark and concurrent sweep collector that does not separate the heap into generations. https://kotlinlang.org/docs/native-memory-manager.html#garbage-collector

  3. “Develop stunning shared UIs for Android, iOS, desktop, and web” – https://www.jetbrains.com/lp/compose-multiplatform

  4. Details about how a KMP project can be used to create an xcframework can be found at https://kotlinlang.org/docs/apple-framework.html, and you can reference our kmp-library-sample project for a concrete example. This is another interesting reversal of the normal way of doing things, where the Swift side of an app typically has source dependencies on other SwiftPM packages and the Kotlin side typically has binary dependencies on jar/aar artifacts published to a Maven repository. When depending on a KMP project, this is reversed: the Swift side has a binary dependency on the xcframework built from the KMP project, and the Kotlin side has a source dependency on the KMP project’s Kotlin/Gradle project.

  5. Note that we could have alternatively depended on a compiled .aar published as a pom to a Maven repository, but for expedience we find that using source dependencies are the easiest way to link directly to another git repository.

The Flutter Kerfuffle

News recently broke that Google is laying off members of the Dart and Flutter teams. At the same time that Microsoft is ending Xamarin support, many Flutter users are left wondering about the future of the technology stack on which they’ve built their apps.

For what it’s worth, we don’t buy into the speculation that Flutter is about the be killed off. While we believe that Skip has the correct approach to cross-platform mobile development, Flutter is an excellent product with a devoted community. A recent post by an insider notes that the Google layoffs were broad and didn’t single out the Flutter and Dart teams. Nevertheless, this episode emphasizes the need to always consider the ejectability of the core technologies you use.

“And every one who hears these sayings of Mine and does not do them will be likened to a foolish man who built his house on the sand.”

A software framework is said to be ejectable if you can remove it without completely breaking your app. Ejectability is not binary: having to remove a given framework might only require you to excise certain features, and some frameworks are easier to replace in our apps than others.

Unfortunately, most cross-platform development tools are not ejectable to any degree. If official Flutter support were end-of-lifed, there would not be any path for Flutter codebases to migrate to another technology stack. Dart is a nice language and Widgets are cool, but Apple is not about to transition from Swift and SwiftUI, nor Android from Kotlin and Jetpack Compose. Languages like Dart and Javascript will forever be “alien” on both iOS and Android, just as Swift and Kotlin will be the primary, supported, and vendor-recommended languages for the foreseeable future.

Flutter apps would continue to work, and the community would likely maintain the Flutter engine and libraries for quite some time. But the pace of integration with the latest Android and iOS features would slow, and businesses who care about the long-term prospects for their apps would look to move on. Unfortunately, their only option would be something seasoned developers avoid for good reason: a complete re-write.

To be clear, we do not believe that Flutter support is going to end any time soon, and the same goes for other popular cross-platform solutions like React Native. But you should also not take the decision to use these frameworks lightly, because they are not ejectable. If support unexpectedly ends - or if a new Apple or Android platform or feature arrives that these non-native frameworks are not able to integrate with - you have no off-ramp.

Skip’s approach to cross-platform development is fundamentally different. You work in a single codebase, but Skip creates fully native apps for each platform: Swift and SwiftUI on iOS, and Kotlin and Compose on Android. This is in keeping with the plainly-stated, unambiguous advice from the platform vendors themselves:

SwiftUI is the preferred app-builder technology -Apple

Jetpack Compose is Android’s recommended modern toolkit -Google

A cross-platform Skip app is a modern iOS app. Your shared code is written in Xcode using Swift and SwiftUI. For example, here is a simple weather model:

import Foundation
struct Weather : Decodable {
let latitude: Double
let longitude: Double
let time: Double
let timezone: String
}
extension Weather {
/// Fetches the weather from the open-meteo API
static func fetch(latitude: Double, longitude: Double) async throws -> Weather {
let url = URL(string: "https://api.open-meteo.com/v1/forecast?latitude=\(latitude)&longitude=\(longitude)&current_weather=true")!
var request = URLRequest(url: url)
let (data, response) = try await URLSession.shared.data(for: request)
return try JSONDecoder().decode(Weather.self, from: data)
}
}

At build time, the Skip tool integrates with Xcode to transpile your iOS codebase into a native Android app written in Kotlin.

As you can see in the sample above, your Swift code does not require any dependencies on Skip. Even integrating Android customization into your app is accomplished without Skip libraries. If you remove all traces of Skip, your iOS app will continue to build and run in Xcode exactly as before.

On the Android side, the Kotlin source code and artifacts that Skip generates are yours, regardless of whether you continue to use Skip. Unlike the iOS app, the translated Android app does rely on Skip libraries to mirror the Foundation, SwiftUI and other Apple APIs that your iOS code likely uses. These libraries, however, are free and open source. Critically, they are not a “runtime” or “engine”. You can continue to use them - or not - without hampering your ability to expand the app and integrate new Android features.

Here is the Kotlin that Skip generates from the sample weather model above. It is not significantly different than Kotlin you’d write by hand. It is longer than the source Swift only because Kotlin does not have built-in JSON decoding, so Skip must add it. (SwiftUI code translated to Skip’s Compose-based UI library for Android is much less idiomatic. If you stopped using Skip you’d likely want to migrate to pure Compose over time, but there would be no immediate need to do so.)

import skip.foundation.*
internal class Weather: Decodable {
internal val latitude: Double
internal val longitude: Double
internal val time: Double
internal val timezone: String
constructor(latitude: Double, longitude: Double, time: Double, timezone: String) {
this.latitude = latitude
this.longitude = longitude
this.time = time
this.timezone = timezone
}
constructor(from: Decoder) {
val container = from.container(keyedBy = CodingKeys::class)
this.latitude = container.decode(Double::class, forKey = CodingKeys.latitude)
this.longitude = container.decode(Double::class, forKey = CodingKeys.longitude)
this.time = container.decode(Double::class, forKey = CodingKeys.time)
this.timezone = container.decode(String::class, forKey = CodingKeys.timezone)
}
companion object: DecodableCompanion<Weather> {
/// Fetches the weather from the open-meteo API
internal suspend fun fetch(latitude: Double, longitude: Double): Weather = Async.run l@{
val url = URL(string = "https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&current_weather=true")
var request = URLRequest(url = url)
val (data, response) = URLSession.shared.data(for_ = request)
return@l JSONDecoder().decode(Weather::class, from = data)
}
override fun init(from: Decoder): Weather = Weather(from = from)
private fun CodingKeys(rawValue: String): CodingKeys? {
return when (rawValue) {
"latitude" -> CodingKeys.latitude
"longitude" -> CodingKeys.longitude
"time" -> CodingKeys.time
"timezone" -> CodingKeys.timezone
else -> null
}
}
}
private enum class CodingKeys(override val rawValue: String, @Suppress("UNUSED_PARAMETER") unusedp: Nothing? = null): CodingKey, RawRepresentable<String> {
latitude("latitude"),
longitude("longitude"),
time("time"),
timezone("timezone");
}
}

(Skip is not a cloud-based tool, but there is an online playground where you can experiment with Swift-to-Kotlin transpilation.)

Skip is fully ejectable. When you eject Skip, you are left with a native iOS app and a native Android app, both using their respective platform vendor-recommended technologies. You can immediately continue to iterate on these apps, with no rewrites and no pause in your ability to integrate new platform features.

We believe that Flutter’s future is secure, despite what some commentary has speculated. You should, however, carefully consider ejectability alongside the many other factors that go into choosing the cross-platform framework that’s right for your business needs.

April Skip Newsletter

Welcome to the April edition of the Skip.Tools Developer Program newsletter!

Skip is the first tool that enables you to build a genuinely native app for both iPhone and Android with a single Swift codebase. This month we have some exciting news, new features and frameworks, and some brand new content for app developers. Let's dive right in!

Early Adopter Discount

First off, we are pleased to announce that the Skip Early Adopter program, which we unveiled last month, is in full swing. For a limited time, the Skip Professional developer program is 50% off, and the Skip Small Business program is 100% off! That's right, if you are a qualifying small business or non-profit, Skip is completely free for the first year.

Take our Developer Survey

We're looking for feedback on the technologies that people are using to build their apps and what features are most important to them. The survey is quick and fun, and lets you have a direct influence over the Skip roadmap during our Early Adopter phase: https://skip.tools/survey

Skip on the Compile Swift Podcast

Abe and Marc were delighted to be invited back to Peter Witham's excellent Compile Swift podcast, where they discussed the present and future directions for Skip, and dove into some interesting technical details of Skip's inner workings, including the secrets to the unbeatable performance of apps that are built using Skip. Check it out at: https://skip.tools/blog/skip-on-compile-swift-podcast

Updated Comparison Table

We truly believe Skip is the best way to build mobile applications that reach the entire marketplace. No other solution offers the same level of user interface polish, raw performance, and seamless integration with the native platform interfaces. We've updated our comparison table to highlight some of the benefits of Skip over alternatives like Flutter, React, and other legacy solutions: https://skip.tools/compare

Skip Module Updates

Skip is more than just a transpiler – it is also an ecosystem of modular frameworks that work identically on both iOS and Android. The open-source frameworks that are maintained by the Skip team are all located at https://github.com/skiptools . Here's a small sample of some of the recent updates to some of our core frameworks:

  • Lottie animations The updated SkipMotion packages enables you to include Lottie animations directly in both your iOS and Android apps. With its impeccably smooth rendering, Lottie has become the de-facto solution for adding whimsy and delight to mobile apps. Discover more about the module at  https://skip.tools/docs/modules/skip-motion  and see the sample application at  https://skip.tools/docs/samples/skipapp-lottiedemo
  • Firebase Firestore Firebase is one of the most popular solutions for storing and synchronizing your data between your devices and the cloud. Our SkipFirestore module is the first part of a comprehensive SkipFirebase package, and sits atop the official Kotlin and Swift Firebase libraries maintained by Google. Read more about the package at  https://skip.tools/docs/modules/skip-firebase  and take a look at the "FireSide" sample app at  https://skip.tools/docs/samples/skipapp-fireside
  • Native SQL The SkipSQL package was recently updated to facilitate using either the vendor-provided sqlite libraries (thereby keeping your app size low), or embedding a full native SQLPlus build, which enables many of the great sqlite extensions that the community has provided over the years: full-text search, encryption, and advanced JSON functions. Read the full documentation at  https://skip.tools/docs/modules/skip-sql  and check out the "Data Bake" sample app at  https://skip.tools/docs/samples/skipapp-databake

Get Your Project Featured

We are assembling a list of Skip projects to feature on our web site. If you have built – or are currently building – an interesting app using Skip, send us an email at support@skip.tools and we may promote it on our customer stories page! And, as always, we are seeking testimonials from happy Skip users that we can share with the rest of the community.

Evaluation Period Extended

For those of you who have signed up for the Skip eval over the past 12 months, we have great news: you can now extend your evaluation period for one additional 30-day trial period. No action is required on your part, you can simply return to https://skip.tools/eval  and request a new key for your host ID, and you will have another 30 days to try out Skip on your project.

That's All Folks!

You can follow us on Mastodon at https://mas.to/@skiptools , and join in the Skip discussions at http://community.skip.tools . The Skip FAQ at https://skip.tools/docs/faq  is there to answer any questions, and be sure to check out the video tours at https://skip.tools/tour .

Happy Skipping!

Bringing Swift and SwiftUI to Android

Swift and SwiftUI are Apple’s recommended technologies for app development, and with good reason. Their emphasis on safety, efficiency, 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 developers’ desire to take advantage of Swift and SwiftUI in other environments too, and advances in Swift tooling have facilitated the porting process. We’re excited to share how Skip combines several Swift platform technologies to bring Swift and SwiftUI development to Android.

Cross-platform development in Xcode

There are multiple paths to supporting Swift on Android, and - like everything in engineering - each comes with its own set of tradeoffs. We chose transpilation as our primary mechanism. Transpiling your Swift source to Android’s native Kotlin language maximizes interoperability, allowing you to call Kotlin and Java APIs directly from Swift - a key concern when you want to take advantage of Android-specific features in your apps.

Our Swift-to-Kotlin transpiler is powered by SwiftSyntax, and we use use Swift Package Manager to both invoke the transpiler as part of the build process, and to integrate our suite of open source Android libraries.

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

The result is a workflow in which you work in Xcode, writing standard Swift and SwiftUI. Our build plugin leaves your source code untouched, but generates, packages, and builds the equivalent Kotlin and Jetpack Compose 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.

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 syntax tree for Kotlin, the modern JVM-based language used in Android development. 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 our output does not look hand-written. Kotlin doesn’t support the expressive ViewBuilder syntax, and Jetpack Compose - Android’s modern 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.

Translating Swift 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.

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.

We maintain a suite of open source libraries to mirror 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.

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 is critical for verifying functionality and ensuring that what worked yesterday will still work tomorrow. This is doubly important for code that runs on multiple platforms.

XCTest is 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.

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

Being able to run a unified set of Swift unit tests across your Apple and Android targets is a critical aspect of any Swift-on-Android solution. 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.

While Swift and SwiftUI are often associated with development for Apple devices, their principles and paradigms are universal, and their use across platforms is spreading. Advances in the Swift ecosystem have unlocked powerful integration possibilities. We leverage these advances to bring Swift and SwiftUI to Android with Skip, allowing you to create fully native cross-platform libraries and apps from a single Swift and SwiftUI codebase.

Skip on the Compile Swift Podcast

We are grateful to Peter Witham for having us come back on his excellent Compile Swift podcast! We discussed the launch of the Skip Developer Program, our Early Adopter discounts, many of Skip’s benefits and how Skip compares to other cross-platform app development solutions, and delved into some low-level development and design topics.

The show is available at compileswift.com, where you can also listen to our previous appearance from last year.

We’re looking forward to appearing again at some point in the future. Peter is a delightful host, and we had a great time chatting with him.

Peter: 00:01 What’s up, folks? Welcome to another episode of the Compulsified podcast or video cast depending on which way you’re listening and watching this. We have some return visitors today. I have Abe and Marc with me, and they have, some tools that have been on before. This is extremely popular episode and they have some new milestones and wanted to come back and you the audience said, yep, we wanna hear about this.

Peter: 00:27 So skip.tools, just very quickly. I mentioned it here. This is you may recall the other episode, I’ll put a link in the show notes, where you can use Swift to make Android applications so you get to live the dream. But I’m gonna let them introduce themselves here. So, Abe or Marc, whichever wants to go first, please jump in and introduce yourself.

Abe: 00:47 Yeah. Hi. I am Abe White and I’m one of the creators along with Marc of skip dot tools.

Marc: 00:55 And I’m Marc Prud’hommeaux. I am the other half of the, of the Skip team.

Peter: 01:00 Great. And we are so thrilled to have you back. I’m not kidding. Everybody was you know, I got a lot of questions, a lot of interest in the tools when you came on before. So this this tells me that you guys are definitely on the right track, but you’ve you’ve got some new milestones and I think when I was looking, I think it was about June last year if you can believe that that we were discussing this.

Peter: 01:24 So let’s go and dive in here. For those who, shamefully have not listened to the last episode, please, tell us what Skip tools are and how it works and what they can do with them.

Abe: 01:38 Sure. I’ll take a crack at it. So Skip is a Swift to Kotlin language transpiler, plus a set of open source libraries, plus an Xcode plug in. So together, what you get is a cross platform framework that lets you create fully native iOS and Android apps from Swift and SwiftUI in Xcode.

Peter: 02:01 And I gotta say I mentioned before on the previous episode, it works amazingly well. I I was expecting to have to jump through hoops. You know, the usual thing. Right? You know, anytime it’s a cross platform kind of scenario, it’s okay.

Peter: 02:17 You know, you you import something or you include it, and you have to do a whole bunch of boilerplate code to make it work. But to my absolute delight it worked first time and, I’m happy to say that even with the documentation because being a developer, of course, I I read just enough documentation which is basically nothing and, tried to spin it up and run it and it worked beautifully and, very performant as well. That’s the other key thing here is that stood out was how well the performance, takes place when you even when you’re running it in the simulator. So talk about that how does this this system actually work? Sort of what talk and and walk us through you you set up your Xcode project but take it from there because I think it’ll be really interesting for folks to realize this isn’t just one of those wrapping a binary around some library.

Abe: 03:21 So, Marc, do you wanna take a crack at

Marc: 03:23 it? Yeah. Well, so Skip, we we come from sort of a different angle as a lot of these other tools where we really embrace embrace the native Xcode workflow of of designing and developing a pure native iOS application using SwiftUI. And we don’t really do anything on the Swift side. And so that’s one of the reasons why, at least, the Swift side is really fast.

Marc: 03:46 And then we are leveraging the fairly relatively new Xcode build plug in system, that sort of augments the entire build workflow by taking your modules written in Swift and, more or less, the background, transpiling them into Kotlin modules, going on a sort of module by module basis. And that really takes advantage of a lot of the efficiencies that the whole Xcode and Swift project package manager system has in terms of being able to compile 1 module just once and not having to to recompile if nothing has changed. And so taking all that together really leads to a very high performance for the iOS side, it’s just as fast as building and deploying a regular iOS application. And then sort of as a secondary effect, you’re you’re transpiling and then compiling using the native Android tools and then running on either the emulator or the device, your transpiled, Android application.

Peter: 04:54 Yeah. And this was something that, like I say when I did it before, it was kind of that skeptical, I guess, thing. Yeah. But what’s the performance really like? You know, but it really is folks.

Peter: 05:10 It it really is, something that you can use it’s not like some of these other cross platforms where you you you see a little lag and and as a developer you’re like, oh, I see what’s going on here. There’s a layer under here doing the translation. But this is either extremely performant that I don’t notice it or as you say it it’s it’s kind of working with the native controls. And I was looking at the website earlier today and the progress that you have made since we last got together, in June last year. And I was looking in particular you have that page with kind of the demo and some of the controls and things like that.

Peter: 05:54 And it’s come a long way. So sort of give us a history of here of what you’ve been working on since we last spoke, progression. I know before as well, we said about there were still some areas of the hardware that were not quite in the system yet. I record the camera, for example, wasn’t in there natively. So what what progress have you made on on the hardware front?

Peter: 06:23 And please point out some of the some of the key things that have really left a long way in the since the last time we got together.

Abe: 06:32 Sure. I’ll start. And I wanna back up a second and address the performance because you mentioned it again. And there are 2 aspects to that. Marc talked about the build performance, which is something that we’ve worked a lot on to make sure that, as he said, iOS is gonna be as fast as it is in Xcode normally because we’re not doing anything.

Abe: 06:49 But on the Android side that we’ve made sure that our transpiler is performant, that it takes advantage of the module system. And we found actually that the Android tools, are very performant as well when they can detect it and nothing has changed in a certain module. And, the other aspect of performance, of course, is is the runtime performance of your app. And, you mentioned that, and that’s something where our our approach of using native code and the native UI toolkits, basically makes us have no lag. On on iOS, of course, we have none at all because we’re not messing with your iOS code.

Abe: 07:27 It’s running as you coded it, the native Swift and SwiftUI. And on the Android side, we’re transpiling into native Kotlin, and our SwiftUI implementation is built on top of native Compose code. So there is a a bit of translation from from the SwiftUI to the compose, but it’s just a small it’s a library on top. It’s not a separate one time. There’s no extra garbage collection.

Abe: 07:50 There’s nothing like that that’s gonna sort of get in the way of the the performance you’d expect of a handwritten, Android app really.

Peter: 07:59 You

Abe: 07:59 know, sorry. And then and then to move on to what we’ve been up to, I’ll let Marc start and I can add on.

Marc: 08:08 Yeah. Well, we’ve been up to a lot. We’ve really since we last talked, fleshed out a lot of the missing pieces of the transpiler so that at this point, we support the vast majority of the Swift language in terms of converting those low level language features into their equivalent Kotlin, structures in in insofar as is possible. You know, Kotlin, in some ways, is not entirely as expressive as Swift, But but you can really you can really go a long, long ways before you run into any of the limitations. We support generics.

Marc: 08:44 We support async await, which was a really big improvement. You know, we support structs. We support all the native swift data types. And that’s on the the transpiler tooling side. And then there’s a whole separate ecosystem of libraries that that we’ve developed, some of which are the equivalent to the libraries that you more or less take it you know, assume are gonna be present for your Swift app, the foundation libraries, and the SwiftUI libraries, and some of the frameworks like, observability and things like that.

Marc: 09:24 So not only those, but also expanding the ecosystem into the sort of utilities that app developers use day to day, like a SQL layer to interact with the the locally installed SQLite database on your phone, that works in exactly the same way on both devices. And and then beyond that, some sort of optional, modules that are extremely popular, like Firebase. We have good support for Firestore now, and sort of the beginnings of some of the other Firebase, array of modules that, that a lot of people are relying on. So yeah. Well, I’ll leave it right there, but that that’s more or less what we’ve been up to in the in the months that we’ve, since we’ve talked to you.

Abe: 10:10 Just gonna say in addition, as a lot of, independent developers know, especially, there’s a lot that goes into productization and making sure that something’s ready for sale. And so Marc, in particular, is in a ton of work on the website and the licensing and purchase flow and just everything that goes into actually moving ChromaTech preview to a released product.

Peter: 10:33 That sounds an awful lot, Marc. Like, Abe is basically saying he made you do the documentation, which, of course, is the bit we all did for. Right?

Abe: 10:40 I actually did a lot of the documentation. We we we collaborated on that, but Marc did have the lion’s share of all those other miscellaneous tasks that go into selling a product, which is, yeah. It’s it’s a mountain of work. Fantastic.

Marc: 10:54 Yeah. Abe Abe has done, I would say, the majority of the documentation, and I’ve probably done a lot of the cobbling together of it all using various Jekyll and PHP and things like that to turn it into sort of the the website that that developers are using day to day to to get the documentation. And that’s actually an area that we’re really proud of. We feel like the documentation is, referencing the the, referencing the the module information and and details about how to package and deploy your applications. Because a lot of a lot of our users are people who are coming to it from a, Swift standpoint and really don’t know anything about Android development.

Marc: 11:43 And it’s a very much a parallel world, but it’s a very sort of bizarro alien world. And so not only do we need to describe how the you know, our own tools work and our own modules work, we also need to make sure they understand how Android works and how you can open your project in Android Studio and debug it there, and how you can package things for deployment, and how you can add permissions and metadata and fonts, actually. That was something I was working on just yesterday. And that that’s all that that’s all really critical, we feel, to the kind of mission of the product.

Abe: 12:20 Time for a break.

Peter: 12:22 Hey, folks. If you like what you’re hearing in this podcast and you wanna help this podcast to continue going forward and having great guests and great conversations, I invite you to become a Patreon supporter. You can go to patreon.comforward/compile swift, where you will get ad free versions of the podcast along with other content.

Abe: 12:43 Break time over.

Peter: 12:44 Yeah. And in fact I’m I’ve I got the website up here and you’ve gone and you’ve got, the the walk through videos as well, which I think is important because as you say for developers, we are notorious for skip through skip through, find what we’re looking for. And and so having it there in video format as well where you can quickly scan through, find the code you’re looking for or go in and refer to the documentation. And and I think I there’s certainly a lot on here compared to the the last time that I I remember looking, you know. And of course, you’ve got the there’s the the blog on there as well.

Peter: 13:24 That’s always nice to see because it’s always nice to see, know, developers communicating with developers. Right? And it’s all great good reference material. And one of the things that I see that’s that’s on there now as well, but I don’t think we covered this last time if I remember rightly, is that you also have the pricing on there now and that licensing structure and it’s nicely formalized because that’s that’s always a concern these days. Right?

Peter: 13:51 Is, not only purchasing licenses, but what does that enable me to do? Is it a for example, it’s is it a a me license? Is it a developer? Is it a per project license? You know, there’s so many different ways these days and I I as I’m looking at it here, you also have an introductory pricing we should point out and and folks there’ll be links in the show notes.

Peter: 14:17 But I’m curious how you came up with what you feel is the the right licensing model and but, yeah. Someone wanna cover that?

Abe: 14:26 Sure. I can certainly. Yeah. So I’ll just quickly outline the current pricing. We have what we’re calling the early adopter release.

Abe: 14:34 So I think when we talked to you last, we’re in tech preview phase. It was just free for everyone, and now we’ve moved to an actual release. It is still an early adopter release, because we do know that certain API isn’t there yet, and, basically, what we’re saying is it’s stable. You can use it to build apps. It works great, but if you’re coming at it from the iOS side in particular, be prepared to learn a little bit of Android for certain things that you’ll probably wanna do that aren’t there yet.

Abe: 15:01 So I think we’ve talked about it last time, but we made that a heavy focus of the whole design of the system is how easy it is to segment right into Android specific code. You can do it right in line in your Swift. You don’t have to set up protocols and all these things. It’s just just start calling Java and Kotlin APIs. You can integrate Compose views.

Abe: 15:24 You can integrate with all sorts of Android specific data structures and things because the transpiler unifies the type systems between the Swift and and Kotlin. So that was it’s always been a focus. What we’re saying with the early adopter releases, you’re gonna take advantage of a little bit of that right now. Most likely, unless it’s a very simple app. So we decided that because we’re in that stage, we wanted to give a discount, and what we came up with right now is, Skip.

Abe: 15:52 First of all, it’s free for any open source project, and that’s a permanent thing. If you’re doing open source, you can use Skip for free. If you’re a small business where you earn less than a certain amount per year, then you can currently also use Skip for free for your 1st year. It’s an annual subscription is the way we’ve licensed it. You can do anything that the product can do with that license.

Abe: 16:17 There are no limitations on number of apps or per deployment or anything like that, and it’s currently free. Normally, we list it as $99 a year for those small business users per developer. And then if you are a larger business, it’s currently 50% off. It’s $500 per developer per year, discounted from a 1,000 normal price. And, Marc, do you want to elaborate?

Marc: 16:46 Yeah. No. Abe really covered all the bases. You know, we we feel like there’s the open source. We feel that that’s a positive thing to offer to people.

Marc: 16:57 And also we get benefit too because people can contribute to our foundational libraries, which are themselves open source. And that’s actually how Skip is able to run on our libraries without needing a license because they are they are under the open source licenses. And then for small business, we figured $99. Everyone’s paying $99 to Apple anyway to build and develop on the iPhone. So $99 to get an equivalent Android, version seemed like it’s a good fit for indies, and also small business and educational organizations as well.

Marc: 17:34 They all fall under this this small small business, $99 a year, currently discounted down to 0. And then the Skip Professional, that’s really for enterprises who are building apps that either are foundational part of their business or are themselves bringing in a lot of money. You know, again, $1,000, that’s not very much. And I’ve done a lot of contracting, and you burn through that in, like, a day of of iOS development. And if you can have a smaller team be able to build your dual platform application with that, we think it really pays for itself in a matter of days, if not hour.

Abe: 18:11 Time for a break.

Peter: 18:13 Hey, everybody. It’s Peter Whittam here from the Compulsory podcast. I wanna tell you about Setapp. Setup is a service that provides a subscription fee of just $10 a month and you get access to over 200 Mac applications and it’s also available now on iOS as part of that deal. I use the service because it just has a ton of really good first rate apps that I use all the time.

Peter: 18:40 And for me, it’s invaluable as a developer to have access to tools for things like APIs, for planning projects, writing emails, writing documentation, and you get all of these things including database apps, all of that kind of stuff right there on the set app service for just $10 a month. You can use as many or as few applications as you need. If you’re interested in checking this out, go to peterwhitham.competerwhitham dotcomforward/setapp, s e t a p p. And you can see the details there, and it’s got a link that you can go over and start using the service and see how it works out for you. I strongly recommend this to every Mac user.

Abe: 19:24 Break time over.

Peter: 19:26 And first of all, thank you for for the open source. You know, open sourcing not only, like you say, the foundation there which works for us as a community, works for you as helping to to grow. But also just I like it when these folks do this open source licensing and there’s some give and take there on both sides is how that’s supposed to work and in return you get to to use this product. So thank you for that. But I I also think that the like you say the small business and the professional, these are extremely good pricing structures, I think.

Peter: 20:09 If if I always look at it and say to myself if I can’t make back the money that I would spend on tools, for like this for example, I have other problems than the what I’m paying for my tools. Right? And especially when you think about that professional level where, I mean I’ve got licenses for products that are are way more than that and it and it’s almost one of those you have to get the license even if you’re not necessarily taking taking advantage of some of the things that it provides you because you’re you’re a large business or something like that. So I think these are very reasonable prices and I’m hoping that folks are are taking advantage of this. And and I’m hoping that you’re seeing a lot of interest from folks and as well a lot of community feedback, I would imagine, as to which direction to focus and how’s that working out?

Peter: 21:13 Are you getting folks who are saying look we really need x y z support? And I know you mentioned Firebase, of course hugely popular big one, right, especially for the Android platforms. So it’s great to hear that that’s in there now. But it sounds to me like you you this is a massive milestone from from when we last spoke. Not only from a technical aspect but, from from a business aspect for you guys as well.

Peter: 21:42 So how’s it been? You know, are you seeing good interest from folks out there?

Marc: 21:47 Yeah. We’ve gotten quite a lot of interest, especially with the early adopter discounts. So we’re we’ve been really excited with with the uptake so far. And as you point out we’re we’re really focused on making sure these early adopters are happy and have everything they need. So really, whenever anyone posts anything, files an issue, posts on our community discussions, or even just emails us, we really bend over backwards to you know, if we’re missing a feature, if there’s, like, a part of the any of the low level modules that needs to be filled in we just go ahead and we put that to the top of our priority list, and and we work through that.

Marc: 22:31 So that’s a big part of this early adopter program is not just that people get a really huge discount, but also they get a lot of say in what winds up getting getting implemented. And as we bring ads that are more and more customers with more and more diverse needs, that’s obviously gonna wind up slowing down a little bit, in terms of just our own bandwidth for filling things in. Yeah. But I I we always try to you know, we always point out to people that there’s the transpiler itself, which is really something that we can only improve. Like, if it if if there’s some feature of the language that, Skip doesn’t yet support, that’s something that we need to implement.

Marc: 23:11 But everything else are all you know, all these modules are open source. Individuals can fork these, and they can fill in that feature that they’re missing. And it’s it’s a lot easier than people often assume. People often think, oh, I need to do all sorts of crazy bridging like you need to do in these other cross platform frameworks and stuff like that. But very often, it’s just a matter of having, like, an if skip block and then dropping down and doing the right Kotlin to get your equivalent feature working, and then just committing it.

Marc: 23:41 And we make a tag release, and everyone gets it. So, so, yeah, we’ve been we’ve been really happy with, with the uptake so far.

Abe: 23:48 Yeah. Fantastic. I wanted to mention when when we talked about Firestore earlier that that is, we call skip 0 implementation. Basically, when you use Firestore through skip, you just code to the Firestore’s regular iOS API. And Marc actually did the implementation there, and he was able to just call out in our Firestore library to the to either the if you’re just using it from iOS side, this is going straight to the Firestore iOS stuff.

Abe: 24:19 But, on the Android side, just call out to Firestore’s equivalent Android API. So all the complexity of the fact that you’re on a different platform is totally hidden, and you’re just coding as if you’re coding an iOS app using FireSource official, API. So you can often do that with with any API that you want to add support for is just look at what it what the iOS API is, and just find the equivalent call you need to make in Kotlin or Java or whatever. And, you get this sort of transparent solution.

Peter: 24:52 And I love that because the the if there’s one thing that frustrates me no end and and it’s because of my limited skills. So I’ll say it before the audience says it. But, it is when I wanna use something and, this happens to me a lot for example with a lot of the game engines out there. And you go to use them and you’re like, okay you I wanna do something that on the surface seems really simple. Like store some information in Firestore.

Peter: 25:22 I’m gonna use that for a leaderboard or something like that. And the next thing I know, I’m diving head first into writing, like you say, these bridging the gap and I no longer feel like I’m writing my app. I’m writing all of this code that I may or may not understand just to get my app to transport the data to the back end and and bring it back again. And there’s nothing worse than, this happens to me a lot. Like I said, the game engines, you’re a few days in and you don’t feel like you’ve gotten anywhere and you’re trying to you’re sitting there saying to yourself, when was the last time I actually did anything on the app itself?

Peter: 26:03 You know. So thank you for solving some of those problems and and working through that. And like you say, it’s I think it’s important that people understand you get to do as the developers, you get to do what you do best. Right? Which is focus on making apps do what you need them to do and making fantastic experiences for people without having to have a whole bunch of boilerplate code in between to make all the various things happen.

Peter: 26:35 And definitely make sure people understand this as well. You know, we’ve we’ve mentioned it but make sure it’s clear for folks. If at any point you can’t do what you need to do in on the Android side, that’s fine. Just go do it in Android. Right?

Peter: 26:52 You know, you can seamlessly blend the 2 as opposed to being forced to like, okay, I’ve got to solve this problem in Swift and try and make it work. And that that is, I think a really important point to make which is if you’re fortunate that maybe you’re an iOS developer with Swift, but you know some Android folks or you’ve got Android developers on the teams and it’s not really your thing, at any point you can jump back over to that native side and let them solve the problem for you there and and just interoperate between the 2. And I think that that’s that’s a really big deal for people to understand. It’s not like some of these other platforms like React Native and things where sometimes they’re opinionated and you you’ve got to do it the way they want you to do it. Right?

Abe: 27:45 Yes. As I said, it was a big focus of development, and it was also something we learned through the tech preview was, getting feedback from some people saying, hey. Like, we we actually want to do more in Android, for various reasons. And, we had the mechanisms in place already, but just wasn’t a point of stress in our sort of documentation and in our examples. And, over time, I think we’ve we’ve stressed that a little more that it’s really up to you which parts you’re doing Android, which parts you’re doing Swift and SwiftUI.

Abe: 28:15 And it’s our job just to make it very easy to blend the 2, and I think we’ve we’ve done a great job doing that.

Peter: 28:21 Yeah. I did too. And like I say for me, talking to you now and seeing where you’re at now compared to where you were last time, and I you know what? I was impressed last time. So to now move on from that technical preview to, I I guess, for one of by putting it, a more confident yes.

Peter: 28:43 You can use this and ship it scenario as opposed to a lot of the times with these early technologies, especially during technical preview, it’s what I call it look for the asterisk. Right? It’s like, yeah, you can use this but your experience may vary. So it’s always fantastic, I would imagine, to feel like you’ve finally checked that box and moved on to that next milestone that says we got something. We feel good about it.

Peter: 29:11 You know, folks start beating on it in in a non lab scenario. Right? Out in the real world, go with it, feel confident about it, and and we’re here to keep moving it forward. So I would imagine you must be absolutely delighted with with being able to check that box and move on to the next thing. Right?

Marc: 29:31 Yeah. Absolutely. And in a lot of ways, it’s a lot easier for us than it is for some of these other frameworks because we don’t actually have a runtime of any sort. You know, we we’re converting your Swift code into Kotlin, but it’s running on all of the native Android SDK on top of Google’s own bulletproof Jetpack Compose. You know, there’s there’s very little that we’re actually doing at runtime, other than our sort of our our libraries that do a lot of work like the SQL library and things like that.

Marc: 30:06 But for the most part, it’s just the only thing that really we’re doing at runtime on the Android side is more or less putting together the pieces of what you want to see happen and what is already implemented in these sort of venerable, very mature preexisting libraries. And that’s in contrast to some other cross platform frameworks that are actually an engine. You know, they’re actually doing things like drawing pixels on the screen. They’re actually having their own run loop. They’ve got all sorts of complexity that they need to deal with, whereas we are more or less just leaning on these both you know, on the iOS side, of course, we do almost nothing.

Marc: 30:50 And on the Android side, we’re just, like, leaning on the existing vendor supported very mature underlying toolkits.

Abe: 30:58 Yeah. That that does allow us to implement things really quickly sometimes. I mean, usually, if people request something that’s missing from, for example, like a SwiftUI thing that’s missing or something like that. You know, it’s it’s a day turnaround maybe to and that’s with all the testing and releasing, and everything else we have to do. And you mentioned the milestone.

Abe: 31:17 Yeah. Just, it is hugely satisfying to have that. It is also interesting, because it means we have to focus on some of the things we’re we’re less experienced or as developers in, which is the marketing side and all those other things that go into having a real product. So, there’s never a shortage, I think, of the things to do. You hit one milestone, and and it’s just on to the next technical one, and at the same time now you have to balance a lot of the business side as well.

Peter: 31:44 Is there anything that we haven’t covered here that you wanted to to bring up? Because here’s the thing. Right? I know you’re gonna have an a next milestone. So we’re gonna be talking again for sure.

Peter: 31:58 But is there anything here that that we haven’t covered that you wanted to mention?

Marc: 32:02 Yeah. Yeah. Actually, I, wrote up a blog post about it, just a few weeks ago, but that’s an area that I personally am very excited with. What you know, as as you probably know, C, Swift has a really good native integration with c. It’s very fluid.

Marc: 32:18 You can just call C functions whenever you want. It’s it’s the way that you would talk to, say, like, SQLite or something like that if you’re just doing a low level a low level inter integration with pre existing libraries. And so one of the things that, I wanted to do is be able to do that on the Android side as well so that we could support really high performance low level things like cryptographic libraries. And also is how we integrate with the equivalent JavaScript core on Android. And it’s also how we do the skip SQL on Android.

Marc: 32:56 And the way that we wound up implementing it is not to get too much into the weeds of Kotlin and Java and how Java uses JNI and things like that to integrate with C. But there’s a library called JNA, which allows Java code to call directly down into C code, and then have callbacks and integrate with structs and things like that. So I implemented this skip FFI layer that more or less eases it doesn’t completely eliminate the complexity, but eases the mechanism by which you can, call into C libraries. And so we use that to integrate with the existing native libraries that are available on on all Android apps. But then on top of that, we also implemented support for, the Android NDK, specifically using CMake build files to be able to build your own C libraries.

Marc: 33:53 So, some examples of that are, like our, skip, LTS for, libtom or ltc, libtomcrypt, which is a set of cryptographic primitives, that’s very cross platform. And so we can drop that into the Swift side, and just call into those libraries. And on the Android side, we use the CMake integration to build it, for each of these supported Android architectures. You know, Android is a little bit different than Swift in that it can be deployed on ARM 32, ARM 64 Intel 32, Intel 64, and then probably at some point in the near future, RISC 5. And so you’ve gotta build 3 or 4 or 5 different versions of every C library that you wanna ship with your Android application.

Marc: 34:47 But it’s really essential for super high performance code because you would never actually do that stuff in Kotlin or in Java. It’s just it’s just not it’s not gonna be fast enough. And so the combination of being able to drop in your own C code into your application, plus Skip’s ability to create that bridging layer. And then it’ll be up to you to have some sort of Swift level encompassing nice API on top of it to make it more ergonomic for your your client developers to use. But it allows you to have super high performance, low level code that’s universal that’ll work on your iOS application and on your Android application.

Marc: 35:34 And that is definitely an area where I think we can continue to explore, and expand in order to have just best in class performance for low level operations. And so that that’s definitely an area that has been a bit of a side project except for the things where we really need it, like skip SQL. But it’s, it’s a really exciting sort of research area that that we’ve been working on.

Peter: 36:03 That’s interesting. And that that’s actually pretty huge because we know developers even if you’ve never done C, you can escape it. Right? At some point, you’ve encountered it even if you haven’t realized it. So it’s huge that at the base level you can say, oh, you want you wanna make this a C library and then just work with it there?

Peter: 36:26 Great. Have at it. Right? You know? And and so that is a a big deal right there.

Peter: 36:31 And I’m sure so many folks out there appreciate that and love the fact that that’s an option for them now. Right? You know, is that they can do this. That’s fantastic and I’m like you’re saying, performance there as well because I know from like the iOS side when we’re talking about dropping down into like objective-C down into C and things like that, that that’s when you’re talking like, okay especially when you’re, say, making games, for example, where you’ve got to squeeze every last little bit of performance out of the rendering engines and texture libraries and and all of these kind of things and like you say cryptography. To be able to drop down to that level and really fine tune that is a really interesting option that takes it way beyond that that higher level of sort of just cross platform technologies and you you can really get down deep in there.

Peter: 37:27 So that’s actually a big deal right there, folks. Yeah. That’s cool.

Marc: 37:30 And that and that goes a lot to sort of our one of the things we feel makes our product fairly distinctive is that we really do have world beating performance not just because we’re embracing, at the at the highest level, the native toolkits SwiftUI, which uses UI kit on iOS, and Jetpack Compose, which uses the native Android view system. But also because you have the possibility of having just absolutely unbeatable performance if you need to go down to that level. And that’s something that you just won’t ever really have if you’re writing your app in, like, JavaScript or Dart or something like that or are constrained by some of the limitations of those of those languages.

Peter: 38:17 Right. You’re not hitting a a bottleneck anywhere. Right? Because because I think that’s the other thing too is anytime we think about something that’s not truly native to a platform, say Android, iOS, whatever your platform is. At some point you’re always thinking, okay.

Peter: 38:35 You know, deep down in here I’m gonna hit a bottleneck.

Marc: 38:38 Yeah. Definitely. And also the other advantage is that while Swift does not have garbage collection and so is is very performant, there’s no escaping the fact that Kotlin and Java do. You just can’t get away from that. And when you’re doing things where you really need to control the kind of memory watermark levels, in order to have an app that doesn’t get kicked out of the system and and is respectful of system resources, then it becomes a lot more important on the Android side to be able to have access to easy access to that kind those kind of levels of performance, assuming you’re comfortable writing in a memory unsafe language like C, which is a big if.

Marc: 39:22 But it’s there for you. You know, there’s there’s no there’s no artificial barrier that prevents you from having having access to that.

Peter: 39:30 Alright. Anything else here?

Abe: 39:32 No. I mean, the last thing I’ll say is just, we’re very thankful to everyone who’s tried Skip and given us feedback. We value that a lot, and I’d encourage, if you haven’t and you’re interested at all go ahead and start your evaluation. That’s totally free, and, it doesn’t even require getting a license or anything to evaluate. And if you want to extend it, even this indie license right now is free.

Abe: 39:55 So, it’s it’s no better time really to to try it out. And please, if you do send us bug reports, send us feedback, things that are working for you, feature requests, all these things. Now is the time because as Marc, pointed out earlier, it’s it’s really a time when when we’re jumping on any feedback we get and making sure that we prioritize that.

Marc: 40:17 I’ll second that because it’s it’s critical to us to know what it is that people people need. You know, some of some of the work that we’re doing at this point where the transpiler is is basically done, is almost as mature as it’s gonna get a lot of the things that we’re doing are things that we are picking and choosing based on what we assume people want. But when these early adopters reach out to us and say, oh, I need this thing done, that’s you know, it goes right to the top of our priority list. And so we really you know, if and when people sign up for the evaluation and for you know, one of the licenses just reach out, talk to us, tell us what you need, tell us what deficiencies you think there are, and and we’ll make it work.

Peter: 41:02 And I did wanna ask. I I forgot to ask. Is do you know is there a a timeline for this pricing before it goes up?

Abe: 41:10 Yeah. I mean, we can say that it it’s definitely a limited time thing, but we don’t have a definite timeline yet. It’s sort of when we feel comfortable, leaving the early adopter label behind and saying this is just a release one.

Peter: 41:24 Yeah. Okay. Alright. Okay, folks. So they’ve they’ve hit this major milestone.

Peter: 41:30 I am super confident they’re gonna hit another one real soon. So, I’m hoping that they’ll they’ll be very gracious to come back, for a 3rd time if they’re if they’re not tired of me already. I don’t know. The accent throws them off. Right?

Peter: 41:43 You know? But Abe and Marc, thank you again so much for coming back. It it was truly an honor to have you the first time. It’s an honour to celebrate the the the progress that you’ve made with you all and again, the the audience absolutely loved the last episode. I’m sure it’s gonna be the same here.

Peter: 42:06 But thank you so much for your time today. And, with that I’ll hand it over to you. Anything that you wanna put in here before we close out?

Marc: 42:16 Yeah. Well, I wanna say thank you so much for having us again on your show. You know, the first one that was really great for us to sort of get the message out there. And now we can invite people to to to really come and and and come to skip. Tools look over the introductory videos.

Marc: 42:37 You can get up and running on it in just a matter of of minutes, really. And then try out your evaluation. We really feel there’s no reason why you can’t bring your iOS app to Android and double your market share. You know, it’s it’s a little bit more work, but the benefits are astronomical. So we really feel like Skip fits a perfect, perfect place, in any mobile developer’s toolkit.

Marc: 43:11 And, and, yeah, we hope to we really hope that we, hear from you the audience, and, and what your experiences are and what you need from Skip. And thank you again.

Peter: 43:22 Alright, folks. Well, that’s what we got for you. It’s, it’s an interesting world that we live in. I always say that. And the the tools like this keep opening up those opportunities for us to explore new things.

Peter: 43:34 And as Marc said you may be building an iOS today, but there’s an untapped market out there on Android if you’ve not thought about it. And these these kind of tools are making that life so much easier for you to to explore that. So go check them out. Again, all the links in the show notes. With that folks, you know where you can reach me and this podcast, Compilescript.com.

Peter: 43:56 And let me know what you think. Right? You know, and if you want, reach out to me and I’ll put you in contact with Marc May but go to skip.tools. And with that, I’ll speak to you in the next one.

Sharing C code between Swift and Kotlin for iPhone and Android apps

Swift — true to its name — is a fast language. It is just about as close-to-the-metal as any higher level programming language can be. Stack-allocated value types make efficient use of resources, and the lack of garbage collection means that memory allocation and deallocation is predictable, with controllable memory watermarks and minimal overhead from reference counting. Swift code is compiled down to architecture-specific machine instructions before being packaged into the app.ipa archive that is ultimately distributed to end users for installation on their iPhone.

Java (and, therefore, Kotlin) is not quite as fast or as efficient with memory as Swift. When a Kotlin Android app is built using gradle or an IDE, it processes the source code into Java bytecode, which is an intermediate representation of the program’s instructions. Next, the Java bytecode is re-processed into Dalvik Executable (DEX) bytecode for packaging into an app.apk for distribution to Android devices. Finally, when a user installs the app, the DEX is converted on the device into architecture-specific instructions, which is what is ultimately run during the program’s execution. The app’s code is run within Android’s managed runtime (ART), which provides automatic memory management using garbage collection (GC).

Skip is a tool that transpiles Swift into Kotlin, enabling the creation of SwiftUI apps for both iOS and Android. The transpiled Kotlin code maintains Swift’s semantics and behavior. But since Java lacks true value types, Swift struct and enumeration types can only be translated into Kotlin classes; this means that they will always be allocated on the heap and subject to indeterministic garbage collection, just like any other Java object. These, and other factors, contribute to the difference in performance and efficiency between the two languages.

Despite the difference in performance characteristics, both these languages are generally “fast enough” for app development. Day-to-day app development in the real world1 typically involves building and tweaking screens of controls and implementing designs, shuffling data between local storage and network services, and generally managing the interaction between a human user and some backend business logic. Rarely do language-level performance limitations become a consideration to a developer who is building an app at this level.

There are, however, many cases where the absolute optimal performance is critical for the app experience to be acceptable. Compression and encryption algorithms, database and format encoders and decoders, and anything to do with real-time video or audio: these are the sorts of things that need the barest-of-metal, lowest-level language there is: C. Nothing, short of hand-written assembly, beats C in terms of capacity for raw performance. And so it is the choice for nearly all the low-level components that are used in mobile operating systems today: the embedded SQLite database, the cryptography libraries, video and audio processing, are all implemented in C on both Android and iOS. Higher-lever wrappers are then built around these C interfaces to make them easy to consume by framework and app developers.

Swift integrates wonderfully with C. Many popular Swift projects use this C integration to great effect, such as the Yams parser embedding the libYAML C library, and the swift-cmark parser wrapping the cmark C library. Parsing YAML and markdown from Swift is so blazing fast because it isn’t implemented in Swift.

Adding your own C code to a Swift project is simple2. Add a new LibCLibrary Swift module and add a Sources/LibCLibrary/include/demo.h header file:

double demo_compute(int n, double a, double b);

and a Sources/LibCLibrary/src/demo.c source file:

#include "demo.h"
double demo_compute(int n, double a, double b) {
double result = 0.0;
for (int i = 0; i < n; i++) {
result += (a * b) / (a + b + i);
}
return result;
}

then add a Sources/LibCLibrary/CMakeLists.txt3 file to bring it all together:

cmake_minimum_required(VERSION 3.1)
project(cproject, LANGUAGES C)
file(GLOB SOURCES src/*.c)
add_library(clibrary SHARED ${SOURCES})
include_directories(clibrary PUBLIC include)

And you now have a C project that can be built with the Swift Package manager.

Next link to the library from a SkipCDemo Swift module, and you can call the C code directly from your Swift code, such as this test case:

XCTAssertEqual(105.95723590826853, demo_compute(n: 1_000_000, a: 2.5, b: 3.5))

Voilà! You’ve now unlocked the full potential for high-performance C libraries on the Swift side of your app. More information on Swift’s C integration can be found at https://developer.apple.com/documentation/swift/using-imported-c-functions-in-swift.

Xcode has built-in support for C. It provides syntax highlighting, auto-completion of function and type names, doc-comment lookup, and inline error reporting. If you are comfortable writing in a memory-unsafe language, Xcode is a productive way to develop cross-platform C code. And developing it alongside a higher-level Swift wrapper with fast unit testing cycles is an effective way to iterate on library development.

Screenshot of Xcode with C project

Smooth Swift/C interoperability is all well and good, but Skip is a tool for making dual-platform apps for both iOS and Android. What of the Kotlin/Java side? Swift’s automatic C interoperability is obviously never going to just work from Java-derived bytecode in the same way as it does from compiled Swift.

However, with runtime support from the SkipFFI framework, the C interface that developers enjoy from the Swift side can be — with some caveats — automatically converted into Kotlin code that interfaces the same way with the native C libraries on Android. It gets close to Swift-level C integration by leveraging the Java Native Access library (JNA), which is a venerable open-source Java library that enables C integration using foreign-function interface (FFI) techniques4. JNA runs atop the standard Java Native Interface (JNI) that provide the ability to pass data and invoke functions between Java and native-compiled languages.

The SkipFFI package uses JNA to provide support for creating native-aware wrapper libraries. The sample project’s wrapper SkipCDemo.swift looks like this:

import Foundation
import SkipFFI
#if !SKIP
import LibCLibrary
#endif
/// `DemoLibrary` is a Swift encapsulation of the embedded C library's functions and structures.
internal final class DemoLibrary {
/* SKIP EXTERN */ public func demo_compute(n: Int32, a: Double, b: Double) -> Double {
return LibCLibrary.demo_compute(n, a, b)
}
/// Singleton library instance
static let instance = registerNatives(DemoLibrary(), frameworkName: "SkipCDemo", libraryName: "clibrary")
}

On the Swift side the DemoLibrary functions are merely passed directly through to the same-named C functions. For the Kotlin side, Skip sees the /* SKIP EXTERN */ statements, and elides the function bodies from the transpiled Kotlin, resulting in a SkipCDemo.kt like:

package skip.cdemo
import skip.lib.*
import skip.foundation.*
import skip.ffi.*
/// `DemoLibrary` is a Swift encapsulation of the embedded C library's functions and structures.
internal class DemoLibrary {
external fun demo_compute(n: Int, a: Double, b: Double): Double
companion object {
/// Singleton library instance
internal val instance = registerNatives(DemoLibrary(), frameworkName = "SkipCDemo", libraryName = "clibrary")
}
}

These extern functions (which need to be public) are what JNA uses to match the functions with the corresponding C functions that it loads from the compiled dynamic library. The result is that the same interface is presented to both the Swift and Kotlin sides of your apps, using identical calling conventions.

In addition to handling the expected primitives arguments and return types, JNA also handles converting between C strings and Java strings. Since Swift strings are treated as Kotlin strings by Skip, and since Kotlin strings are just aliases to Java strings, string arguments and return types generally work transparently when zero-terminated C string conventions are followed.

On top of all this, the SkipFFI package provides some stand-in mappings for the Swift C interoperability types. For example, the Swift.OpaquePointer5 type will be represented by a typealias to the com.sun.jna.Pointer6 type. This is generally enough to create a portable Swift wrapper around a C type that is represented by a pointer.

Beyond functions, JNA has support for mapping structs and union types, as well as working with low-level shared memory. These are not yet directly supported by Skip (see “Future Work” below), but they can all be overcome by embedding Kotlin directly in #if SKIP / #endif blocks.

SkipFFI is how the the SkipSQL framework interfaces with the vendored SQLite API on Android and iOS. It is also how SkipScript works with JavaScriptCore, which is included with iOS but needs to be shipped as an additional library with Android apps. Regardless of whether the dependent library is included with the operating system or shipped separately, the mechanism for interacting with it will be the same. So If you already have pre-built native libraries for iOS and/or Android that you can include in your project, then SkipFFI is all you need to be able to access them from the Kotlin side.

However, if you want to include C code directly in your project, you’ll need to build that code for the Android side and package it in a way that makes it accessible at runtime, in the same way as SwiftPM handles building and packing it for iOS.

Compiling C into Android apps with the NDK

Section titled “Compiling C into Android apps with the NDK”

As of release 0.7.44, the Skip transpiler will generate a gradle project that will cross-compile your C project to each of the four Android native architectures and embed it in the resulting app.apk. Skip detects native support by checking for the following declaration in the target module:

cSettings: [.define("SKIP_BUILD_NDK")]

This causes Skip to create a build.gradle.kts project that uses the externalNativeBuild clause, directing it to use the CMakeLists.txt to build the native libraries for each of the supported Android NDK targets.

The generated build.gradle.kts project will look something like this:

plugins {
id("com.android.library") version "8.1.0"
}
android {
namespace = group as String
compileSdk = 34
defaultConfig {
minSdk = 29
}
externalNativeBuild {
cmake {
path = file("ext/CMakeLists.txt")
}
}
}

When building and testing the project, the Gradle project will be generated for the native library, which will instruct gradle to build the project with each of the four separate Android Native Development Kit (NDK) toolchains. The first time you do this, the toolchains will be automatically downloaded and installed as part of the build process, so you should expect the initial build to take a much longer time than subsequent builds.

As you can see in the Xcode Console area, when running the skip-c-demo test cases (and thereby running the transpiled Kotlin tests via gradle), the native compilation steps look like:

GRADLE> > Task :LibCLibrary:buildCMakeDebug[arm64-v8a]
GRADLE> > Task :LibCLibrary:buildCMakeDebug[armeabi-v7a]
GRADLE> > Task :LibCLibrary:buildCMakeDebug[x86]
GRADLE> > Task :LibCLibrary:buildCMakeDebug[x86_64]
GRADLE> > Task :LibCLibrary:mergeDebugJniLibFolders

The intermediate LibCLibrary.aar archive will correspondingly contain the embedded compiled shared object files:

classes.dex
lib/arm64-v8a/libclibrary.so
lib/armeabi-v7a/libclibrary.so
lib/x86/libclibrary.so
lib/x86_64/libclibrary.so
AndroidManifest.xml
resources.arsc

Each of these shared object files will be distributed as part of the final app.apk, although note that if you distribute your app as part of the an Android Bundle, then the app storefront may be able to thin out unused architectures before it delivers your app bundle to the end user for installation.

At runtime, the SkipFFI library will handle loading the shared object file that you specify by name in your wrapper class, and you’ll be able to interact with the C library via the wrapper. JNA will link up the function names (or throw an exception for any external functions that cannot be found), and the library will thus be ready for consumption by the Kotlin side of the calling code.

The ability to invoke C directly from both the iOS and Android side enables the prospect of unbeatably-fast dual-platform libraries that can be consumed by app and framework developers alike. We’ve only scratched the surface of what is possible, but to delve deeper, start with the https://github.com/skiptools/skip-c-demo sample repository and the SkipFFI documentation. The SkipTidy repository shows a non-trivial example of an existing C library being embedded in a dual-platform Swift framework in source form. And for examples of using SkipFFI with pre-compiled libraries (either included with the OS or bundled with the app), see the SkipSQL and SkipScript frameworks. The SkipFFI test cases also show some examples of integrating with OS-embedded libraries like zlib and libxml, as well as manually mapping C structures to Swift types.

We’re very excited to see the capabilities that will be unlocked by developers using this feature! If you have questions or suggestions for improvements, please reach out to us on Mastodon @skiptools@mas.to, via chat skiptools.slack.com, or in our discussion forums.


  • structs: automatic C integration using SkipFFI only works for function invocations. C structs, which are nicely mapped to Swift structs, do not benefit from any automatic conversion on the Kotlin side. It is possible to manually implement them using the JNA Structure interface, but it is currently cumbersome. For an example, see the ZlibLibrary test case in the SkipFFI tests.
  • C++: Swift recently gained the ability to integrate with C++ code. SkipFFI and JNA lack any understanding of C++, so while you can build and embed your C libraries using SwiftPM and gradle, you’ll need to interface with it through a C API.
  • JNA Overhead: while there is essentially zero overhead when calling into C from Swift the side, on the Kotlin side the JNA layer does introduce some overhead. When a JNA wrapper class is first instantiated, the runtime needs to do some work to load the correct shared libraries and match up the extern functions with their corresponding C functions. This mostly affects the initial load time, and single a library wrapper instance is likely to be a shared singleton for an application, this overhead is usually tolerable.
  • Size overhead: the SkipFFI depends on the JNA library, which itself includes some native shared object files to facilitate the C side of the bridge. This adds about 1.2 MB to the size of the compiled app.apk, but note that the resulting app size delivered to end users might be smaller if the app storefront is able to thin out unused architectures.
  • Unsigned types: Neither the Java language nor the Java bytecode specification have any support for unsigned types. Kotlin’s support for unsigned types (e.g., UInt8) are wrappers around the equivalent signed type (e.g., Int8), and are not understood or handled by JNA. Attempting to map a function parameter or return type to an unsigned data type will raise a runtime error. In order to work around this limitation, unsigned types must be manually marshalled and unmarshalled, which can be cumbersome.
  • Project Panama: this recent project aims to ease the interaction between Java and foreign APIs written in C and C++. It is presented as an alternative for JNI and JNA, and eliminated most of the bridging overhead, making it the most high-performance call interface from Java into native code. However, Project Panama is an OpenJDK project, and is unlikely to make it into Android’s Java runtime (ART) anytime soon.
  • Mach-O/ELF: You may be wondering what the difference between the libSkipCDemo.dylib and arm64-v8a/libclibrary.so are. If you’re building on an ARM macOS machine, the architecture for both these files should be arm64-v8a, right? Yes, but they are not the same format. Darwin-based operating systems like iOS and macOS use the “Mach-O” executable format7, whereas Linux-based operating systems like Android use the “ELF” format8. You can see this after running swift test in the sample project from the Terminal:
skiptools/skip-c-demo % file ./.build/arm64-apple-macosx/debug/libSkipCDemo.dylib ./.build/plugins/outputs/skip-c-demo/LibCLibrary/skipstone/LibCLibrary/.build/SkipCDemo/intermediates/merged_native_libs/debug/out/lib/*/*.so
…/libSkipCDemo.dylib: Mach-O 64-bit dynamically linked shared library arm64
…/arm64-v8a/libclibrary.so: ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked
…/armeabi-v7a/libclibrary.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked
…/x86/libclibrary.so: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked
…/x86_64/libclibrary.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked
  1. Non-game development, that is.

  2. More information on Swift’s C integration can be found at https://developer.apple.com/documentation/swift/using-imported-c-functions-in-swift.

  3. The CMakeLists.txt is used by the cmake tool (https://cmake.org), a flexible and popular native build tool that is used by both Swift Package Manager and the Gradle Android plugin (see https://developer.android.com/ndk/guides/cmake) for integration with native build systems.

  4. The details of how this works can be found at the JNA project’s homepage at https://github.com/java-native-access/jna. JNA adds about 1.1 MB to the side of your app.apk, which is why it is included in a separate SkipFFI package rather than included directly with SkipLib or SkipFoundation.

  5. Swift C interoperability types5 (https://developer.apple.com/documentation/swift/c-interoperability) 2

  6. Javadoc for com.sun.jna.Pointer: https://java-native-access.github.io/jna/5.14.0/javadoc/com/sun/jna/Pointer.html

  7. Mach-O file format: https://en.wikipedia.org/wiki/Mach-O

  8. Executable and Linkable Format (ELF): https://en.wikipedia.org/wiki/Executable_and_Linkable_Format

Skip 2024 Roadmap

Skip enables you to build the best possible apps, for the widest possible audience, using a single codebase. Our goal with Skip is to enable individuals and small teams to create apps for both the iPhone and Android using the native first-party toolkits for those platforms: SwiftUI and Jetpack Compose. We believe this is the best possible experience for users of iPhone and Android devices.

We spent 2023 building the underlying technology to enable this project. The SkipStone transpiler takes your code written in Swift for iOS and converts it into Kotlin for Android. The open-source SkipStack frameworks provide runtime support: they transparently bridge the various Darwin and iOS software layers into their Android equivalents. This includes SkipUI, which takes your SwiftUI interface and turns it into the equivalent Jetpack Compose code, as well as SkipFoundation, which bridges the low-level Foundation framework to the Java APIs used by Android apps. Bringing it all together is the Skip Xcode plugin that automatically runs the transpiler when you build your project and converts your entire Swift package into a buildable Android gradle project.

The end result is a magical development experience: you develop in Xcode and run your SwiftUI app in the iOS Simulator, and Skip seamlessly transpiles and launches your Compose app in the Android emulator. Change some code and re-run, and within seconds both your iOS app and Android app are rebuilt and run side-by-side, ready for testing. The Skip Tour video provides a taste of this process.

In 2024 we will be expanding our ecosystems of Skip frameworks beyond SwiftUI and Foundation. Aside from user interface widgets and operating system integration, a modern mobile app needs a variety of capabilities: graphics and animation, SQL databases, push notifications, media players, cloud storage, payment integrations, parsers for common data formats, cryptography, game technologies, et cetera.

There are many existing libraries, both 1st and 3rd party, for both Android and iOS that fulfill these needs. Our goal is not to re-invent them, but rather to build common abstractions atop them. For example, the new SkipAV framework for playing music and videos is not created from scratch, but is rather implemented on top of iOS’s AVKit and Android’s ExoPlayer. This enables Skip apps to take advantage of each platform’s best-in-class libraries that have matured over the years, while at the same time maintaining a single dual-platform API for developer convenience.

These frameworks are all free and open-source software that Android and iOS app developers can use in their apps. We sell the Skip transpiler itself, but the ecosystem of Skip frameworks can be used – independently of the Skip transpiler – whether or not you are a customer. We choose to make them free software not merely as a value-add for our own customers, but also to grant you, the developer, the confidence that anything you build with Skip will remain under your purview, and that you retain the agency to continue to iterate on your app, with or without the Skip transpiler.

The realization of genuinely native apps for both major mobile platforms, created from a single codebase in a single language, has been a dream for a long time. 2024 will be an exciting year.

Skip is Free for Free Software

Skip brings your iPhone app to Android. With Skip, you can create a modern SwiftUI app with the standard iOS development tools, and Skip transforms it into a Kotlin app for Android. With Skip you can iteratively design, build, test, run, debug, and deploy a single app for both major mobile platforms using a single language (Swift) and a single IDE (Xcode). Watch our 12-minute tour for a glimpse of the magic.

Today we are pleased to announce that Skip will be free for all free open-source software.

There are two halves to the Skip project. The first is SkipStone, our integrated Xcode plugin that transpiles your Swift source code into Kotlin as part of the normal build process. SkipStone is commercial software. It is currently a public technology preview, with early adopter pricing to be announced soon1.

The other half is Skip’s ecosystem of libraries and frameworks, which is free and open-source software. These frameworks constitute the essential building blocks of any modern application, and include low-level adaptors from Darwin’s Foundation API to the equivalent Android Java API (skip-foundation), as well as the high-level SwiftUI user interface widgets that are manifested by Jetpack Compose views on Android (skip-ui). In addition, the growing constellation of community frameworks at github.com/skiptools provides essential functionality such as SQL database support, media player components, and more.

These frameworks are free and open-source software whose advancement will rely heavily on community contributions. And so we’ve made the SkipStone transpiler free, for free software. What this means is that Skip can be used – without cost – for building projects that consist exclusively of source code licensed under one of the General Public Licenses as published by the Free Software Foundation.

This applies not just to framework development, but also to your own app projects: if your iOS app is free software, then Skip can be used to transpile it into an equivalent free Android app. In this way we hope to encourage and support the proliferation of genuinely native dual-platform apps created with the Skip transpiler and powered by the community-supported ecosystem of high-quality libraries and frameworks.

Skip is advancing by leaps and bounds, but it is still a technology preview. You can use it today to create a greenfield app for iOS and Android, provided you are willing to iterate carefully and to work around (or implement and contribute!) some missing pieces. Read the getting started guide to begin the adventure.

The reward will be well worth the effort. Your genuinely native app, created from a single modern Swift codebase, running on both Android and iOS, will be priceless.

More details can be found at our FAQ and documentation. Follow us on Mastodon at mas.to/@skiptools or with RSS.

  1. Commercial pricing will be announced soon, but you can qualify for an early adopter discount by registering during the tech preview period.

Announcing the Skip Technology Preview!

We’re thrilled to announce the tech preview of Skip: dual-platform app development in Swift.

Screenshot

You write a modern iPhone app, and Skip transpiles it into a native Android app in real time.

Skip is the only solution that enables the creation of genuinely native apps for both iOS and Android with one language, one team, and one codebase.

Check out the video tour and visit the FAQ and documentation to get started. Sign up at skip.dev to be notified about future updates.

Happy Skipping!