Skip to content

Blog

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.

June Skip Newsletter

Welcome to the June 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.

New Skip Intro Video

We've posted a new Skip "Showreel" video, providing a quick 3-minute overview of Skip and the highlights of using it to build native dual-platform apps. You can find it on YouTube at: https://www.youtube.com/watch?v=lQjaaAqgxp4. This and other videos are also available from our Tour page at: https://skip.tools/tour/. We will be posting new videos in the coming weeks and months, so consider either following us on YouTube, or subscribing to our RSS feed from https://skip.tools/blog/.

skip-showreel-poster.png

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 as well as the Google Play Store. 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 Framework: SkipKeychain

Using the Keychain has long been the standard way to store bits of sensitive data, such as passwords and notes, on your iOS device. We're happy to announce a brand-new SkipKeychain module that provides an API to read and write sensitive data both on iOS and Android. As with the rest of Skip's library ecosystem, it is free and open-source and available on GitHub at: https://github.com/skiptools/skip-keychain/. We're only on version 0.0.1 right now, but we expect to be able to iterate quickly to add features and functionality that the community wants to see in this nascent project.

Skip and Kotlin Multiplatform

Skip and Kotlin Multiplatform (KMP) are two sides of the same coin. Skip brings your Swift/iOS codebase to Android, and KMP brings your Kotlin/Android codebase to iOS. Many people have assumed that this diametrical opposition means that the two technologies are incompatible. But this is not the case! KMP modules can be embedded in Skip apps, and they work seamlessly, for the most part, with the Swift-to-Kotlin code transpilation that Skip provides. Check out our deep dive into the integration at https://skip.tools/blog/skip-and-kotlin-multiplatform/ and learn how you can take your business-logic KMP modules and integrate them in both the iOS and Android sides of your Skip app.

Skip Slack Group

By popular demand, we are starting to migrate away from our gitter.im Matrix chat system to a new Skiptools Slack group. Going forward, this will be the preferred medium for live discussions and getting technical help. The Skip team will be standing by to answer questions and help with any issues that members of the community may encounter. You can sign up and join the conversation at: https://skip.tools/slack/.

Skip and Fastlane

The last mile of app development can be the most grueling. Taking your tested and polished 1.0 app and getting it into the hands of your users ought to be quick and simple, but it isn't. Running the gauntlet of the app store submission process is hard enough when you only target one platform, but when you target both iOS and Android, you need to contend with a plethora of hurdles for both the Apple App Store and the Google Play Store.

Fortunately, the popular Fastlane tool ( https://fastlane.tools/ ) has evolved over the years to help alleviate some of the drudgery of submitting new apps, as well as updated releases, to these storefronts. And we're happy to report that new projects created with the `skip init` command will now include Fastlane templates that provide everything you need to automate your app distributions. Read more about it on our blog post: https://skip.tools/blog/skip-and-fastlane/.

WWDC, Google I/O, and Skip

Google I/O 2024 and WWDC 2024, the preeminent conferences for Google and Apple developers alike, went back-to-back in June. These exciting events unveiled a lot of new features to the languages and frameworks that are used daily by mobile app developers. We here on the Skip team are working hard to take advantage of many of the new features that were announced.

Language evolution was announced as well: Kotlin 2.0 (final) was released, and Swift 6.0 (beta) was offered up in preview. As we march towards a Skip 1.0 release, we are going to make sure that all the code we process and generate is compatible with both these next-generation language releases, and takes advantages of as many of the new features as possible, while still remaining compatible with prior source and binary releases. Skip 1.0 is right around the corner, and it will be right up to date with the latest and greatest!

Thanks!

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

Going the last mile with Skip and Fastlane

Getting your finished app into the hands of users can be a laborious process. The individual app stores – the Apple App Store the the Google Play Store, primarily – have their own cumbersome web-based processes for uploading the app binary, adding metadata and screenshots, and providing the necessarily content ratings and regulatory information required by various jurisdictions. And once you have gone through all the tedious manual steps needed to release the initial version of the app, each and every update will also need to follow many of those steps all over again.

Fortunately, this process has become so irksome, to so many developers, that a community tool called “Fastlane” was born. In the documentation, it describes itself as:

fastlane is the easiest way to automate beta deployments and releases for your iOS and Android apps. 🚀 It handles all tedious tasks, like generating screenshots, dealing with code signing, and releasing your application.

Fastlane is architected as a collection of plugins that handle all manner of app distribution tasks, like packaging, signing, and uploading. It is configured with a platform-specific hierarchy of local text and ruby configuration files, one for Android and another for iOS.

Skip 0.8.50 now has built-in support for creating a default fastlane configuration for each of your iOS and Android projects. When you create a new project with the command:

skip init --fastlane --appid=app.bundle.id package-name AppName

The Darwin/ and Android/ folders will each contain a template for the fastlane project, which holds the metadata files that can be edited to fill in information like the app’s title, description, content ratings, and screenshots.

Once you fill in the generated text files with your app’s specific details, such as the title, description, and keywords, you can then use the fastlane release command in each of the folders to create new releases of your app and submit them either to the store’s beta test service, or to be reviewed for worldwide release.

Being able to quickly build and upload a new release with a single command is a great help in maintaining a rapid release cadence. It enables “continuous delivery”, defined by Wikipedia as:

Continuous delivery (CD) is a software engineering approach in which teams produce software in short cycles, ensuring that the software can be reliably released at any time. It aims at building, testing, and releasing software with greater speed and frequency. The approach helps reduce the cost, time, and risk of delivering changes by allowing for more incremental updates to applications in production. A straightforward and repeatable deployment process is important for continuous delivery.

We are using this process ourselves with those Skip apps that we are delivering to the app stores, like the Skip Showcase app that demonstrates the various SkipUI components (App Store link, Play Store link).

Being able to submit a new release to both the major app storefronts with a single command is a joy. It can be used to submit quick fixes, or as part of a continuous integration workflow triggered by tagging your source release. However you use it, Fastlane eliminates much of the repetition and tedium of release management.

For more information on Skip’s Fastlane integration, see the deployment documentation.

May Skip Newsletter

Welcome to the May edition of the Skip.tools newsletter! This month we will showcase some of the notable improvements to the Skip transpiler and the ecosystem of free and open-source frameworks that power the dual-platform apps that Skip enables.

Early Adopter Pricing Ending Soon

Skip 1.0 is on the horizon, which means that we will be winding down our Early Adopter Program at the end of the month. So now is the time to take advantage of the massive early adopter discount from https://skip.tools/pricing , as Skip will be switching to full pricing next month.

New Sample App: Travel Bookings

We've released a whole new sample application that shows off how Skip can make gorgeous apps for both iOS and Android. The Travel Bookings app demonstrates navigation, tabs, images, persistence, maps, weather, networking, and a whole lot more. Check it out at /docs/samples/skipapp-bookings .

skip-splash-poster.png

Symbols and Images in Asset Catalogs

Images and icons are an essential part of any modern application. Skip has had good support for SwiftUI's AsyncImage for a while now, but we recently also added support for asset catalogs, enabling you to bundle static images and exported symbols directly in your app. And we support many of the common variants for assets, such as light and dark variants for images, as well as different weights for symbols. Read more about the new asset catalog support at /docs/modules/skip-ui/#images .

Major Performance Enhancements

We are delighted to report that we’ve reduced the number of re-compositions SkipUI performs on Android, resulting in a huge performance boost to some common operations like navigation. If your tabs or navigation bar was feeling a bit sluggish, run File / Packages / Update to Latest Package Versions on your project to grab skip-ui 0.9.1 and enjoy the speed boost!

Tip: Embedding Kotlin Calls Directly in Swift

Unlike other cross-platform app development frameworks, custom native integration in Skip is a breeze. Rather than requiring cumbersome bridging infrastructure or platform channels, with Skip you merely add your Kotlin calls in an `#if SKIP` block, and it will be executed directly on the transpiled side. And since Skip does not intrude into the iOS side of your app, you'll continue to be able to integrate with any of the Darwin platform APIs directly, including UIKit and other Objective-C frameworks (as well as C and C++). Read more about the platform customization options at /docs/platformcustomization .

Accessibility Improvements

May 16th was Global Accessibility Awareness Day. Skip celebrated by adding support for many additional SwiftUI accessibility modifiers. Being a truly universal app means not just reaching all the devices that people have, but also making those apps usable by everyone. Skip is proud to enable you to build uncompromisingly excellent apps that can reach the entire world: every device, every language, and every ability.

Take Our Survey!

Our Skip Developer Survey is a great way to provide feedback and help us define Skip's direction in the coming weeks and months. It only takes a few minutes, and will help define Skip's focus and features: https://skip.tools/survey .

Edge-to-edge Mode

In the "Improve the Experience of your Android App" session at Google I/O, the Android team promoted the use of the new edge-to-edge support APIs in Jetpack Compose, saying that “users significantly prefer edge-to-edge screens to non-edge-to-edge screens, and users feel these screens are more satisfying and premium.”

We agree, and so Skip now enables Android edge-to-edge mode by default in all new projects. Use the SwiftUI safe area APIs to control how your content renders under system bars. And you can enable edge-to-edge in existing projects with only a couple lines of code: /docs/modules/skip-ui/#enabling-or-disabling-edge-to-edge .

Skip Webinar Series

Sign up for our Skip webinar to see a hands-on tour of how Skip can help you build apps that reach the entire mobile marketplace. We take questions and answers throughout, so this is a great opportunity to get some direct interaction with the Skip engineers. Sign up at https://skip.tools/webinar/ or watch a past webinar at /tour/ .

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 customers page! And, as always, we are seeking testimonials from happy Skip users that we can share with the rest of the community.

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 /docs/faq/ is there to answer any questions, and be sure to check out the video tours at /tour/ .

Happy Skipping!