Menu

Development

Skip allows you to share as much or as little code as you want between the iOS and Android versions of your app. The Platform Customization chapter details how to integrate Android-specific or iOS-specific code. This chapter focuses on shared dual-platform development.

In general, dual-platform Skip app development is modern iOS app development. You work in Xcode. You code in Swift and SwiftUI. As much as possible, Skip’s goal is to disappear.

The following sections do not attempt to teach you iOS development. There are other available resources for that. Rather, we focus on where dual-platform Skip development differs from standard iOS development, including how to use Skip’s tools and what to do when things go wrong.

Philosophy

The biggest mistake that new Skip developers make is to assume that a Skip build error means that they cannot use a particular iOS feature. With Skip, that is never the case.

We designed Skip from the ground up knowing that all multi-platform tools have limitations, and Skip - especially as a young product whose support is still expanding - is no exception. That is why we concentrated on making it trivial to exclude unsupported iOS code from transpilation. You can truly use any iOS features, right inline, without compromise and without re-architecting your app.

Of course, using an iOS feature without Skip Android support means you’ll need to provide an Android fallback or alternative. Which is why Skip also makes it easy to integrate Android-specific solutions, whether to work around limitations or to differentiate your Android experience.

So remember: Skip build errors show you what may not yet be supported out of the box on Android. They might require extra work to overcome, but they are not blockers!

Building and Running

In order to run and test your app on Android, you will need either an Android emulator or a paired Android device with developer mode enabled. You can set up an emulator by running Android Studio.app, opening the Device Manager from the ellipsis menu of the “Welcome” dialog, and using the Device Manager to create an emulator of your choice. You an then use the Device Manager to launch the emulator, or you can run it from the terminal with a command like ~/Library/Android/sdk/emulator/emulator @Pixel_6_API_33.

Screenshot of the Android Studio Device Manager

Android Studio.app does not need to be kept running in order to use the Android emulator, but it can be useful for attaching the adb debugger to the running process.

Apps

Assuming you followed the app creation instructions using skip init, each successful build of your Skip app will automatically attempt to launch it on the running Android emulator or device. Exactly one emulator or device must be running in order for the Skip project’s Launch APK script phase to install and run the app successfully.

If you are having trouble with Skip’s Xcode plugin, check the Troubleshooting section for help.

Frameworks

Building a dual-platform framework in Xcode builds your iOS code and runs the SkipStone transpiler. It does not, however, perform an Android build. Due to limitations on Xcode plugins, the only way to invoke the Android compiler is to run the module’s unit test suite against the macOS destination. Skip adds a special unit test in XCSkipTests.swift that compiles the transpiled Kotlin source code and also transpiles and runs your XCTest tests as JUnit tests.

Framework Test Development Screenshot

Parity tests are only run for the macOS destination. For more information on parity tests, see the Testing documentation.

Running on an Android Device

To run on an Android device rather than the emulator, pair the Android device with your development machine and set the ANDROID_SERIAL variable in your project’s .xcconfig file to the device’s identifier. Running the /opt/homebrew/bin/adb devices command will show the available paired identifiers. Setting ANDROID_SERIAL is also the only way to run the app when there are multiple emulators running.


Coding

Writing dual-platform code with Skip resembles coding a standard iOS app, and it can feel magical to see your Swift and SwiftUI run on Android. But in the end Skip is a tool that you work with, and writing for two platforms does introduce complications not found in pure iOS development:

  1. While Swift and Kotlin are similar in many ways, there are some language differences that Skip cannot resolve. Additionally, the Skip transpiler’s type inference engine is not as sophisticated as that of the full Swift compiler. As a result, you will sometimes have to adjust your Swift code so that it can be transpiled into valid Kotlin. Typically, Skip’s Xcode plugin will tell you when and why this is necessary when you build. For more information on what Swift language features Skip supports, see the Swift Support Reference.
  2. At some point, you will likely find yourself wanting to use an iOS API, framework, or feature that is not yet supported on Android. Don’t give up! This section discusses your options when you encounter a limitation in Skip’s dual-platform coverage.
  3. Writing a dual-platform apps means using dual-platform libraries. As part of its unique transparent adoption, Skip automatically maps your Swift imports of standard iOS frameworks to the correct Skip library for Android, so that your Swift code doesn’t depend on Skip. For example, import Foundation in Swift becomes import skip.foundation.* in Kotlin. Our documentation on dependencies discusses how to use other dual-platform libraries as well as iOS and Android-specific libraries.

Transpiler Errors

Skip tries to warn you as quickly as possible when you’re going down the wrong path, and so the Skip transpiler may report warnings and errors even before Skip attempts to compile the generated Kotlin. Like Swift compiler errors, each message typically appears in two places: once inline in your Swift source code, and once in Xcode’s sidebar issue navigator. Clicking an entry will jump you to the offending code.

The most common transpiler errors are:

  • Using an API without Android support. We discuss your options when an API you want to use has not yet been ported to Android below.
  • Type inference failures. Unlike the Swift compiler, Skip’s transpiler is often able to translate code even when it doesn’t understand the APIs and types involved. This is particularly useful when calling Kotlin and Java API from Swift. In some cases, however, the transpiler must be able to determine the exact types you’re using:
    • When you use an unqualified member name, as in let isMax = i == .max or process(person: .init(name: "Skip"))
    • When you’re calling async code
    • When you’re in a SwiftUI @ViewBuilder

You may get warnings or errors from the transpiler in these situations. Typically, the root cause of these issues is that you’re using some unsupported API, so Skip loses its ability to track the types involved.

You can work through transpiler errors by finding appropriate solutions immediately, or by using compiler directives to exclude the problematic sections of code from your Android build until you’re ready to tackle them.

Kotlin Compiler Errors

When the Kotlin compiler reports an error in the generated Kotlin code - often due to using an unsupported API - Skip attempts to map the error back to your Swift source. So just as with transpiler errors above, you will typically see the error reported both inline in your Swift code and in Xcode’s sidebar issue navigator. This time, however, the navigator will contain two entries: one referencing the Swift file and line number, and another referencing the Kotlin file and line number. Clicking either entry will jump you to the offending code in the corresponding Swift or Kotlin source file.

Framework Development Screenshot

Structs, Garbage Collection, Numeric Types, and More

Swift and Kotlin differ in more than just syntax. The Special Topics section of the Swift Language Reference covers how Skip addresses deeper differences like value types vs. reference types and reference counting vs. garbage collection. We strongly encourage you to read these topics, as they will help you understand how your code will behave on Android and keep you from making many common mistakes.

Logging

Messages that you print do not appear in Android logging. Instead, SkipFoundation includes support for importing OSLog and using the standard OSLog.Logger API for dual-platform logging:

import OSLog
...
let logger = Logger(subsystem: "my.subsystem", category: "MyCategory")
...
logger.info("My message")

When you log a message in your app, the OSLog messages from the Swift side of the app will appear in Xcode’s console as usual. The Kotlin implementation of OSLog.Logger, on the other hand, forwards log messages to logcat, which is Android’s native logging mechanism. Using the logcat tab in Android Studio is a good way to browse and filter the app’s log messages on the Android side. You can also view logcat output in the Terminal using the command adb logcat, which has a variety of filtering flags that can applied to the default (verbose) output.


iOS Features without Android Support

Skip is continually adding functionality, but there are still many iOS APIs, frameworks, and features that are not yet implemented for Android.

With Skip, you never have to compromise your iOS app. You can always use Skip’s compiler directives to exclude unsupported iOS code from transpilation. Then create an alternate Android code path that either falls back to a supported solution or implements a solution using native Android API.

API

Using an iOS API that is not yet supported on Android will result in either an unavailable API error from the Skip Xcode plugin, or an unknown reference error from the Kotlin compiler. These errors will come at build time.

Check each of Skip’s modules to learn more about its current status and Android-supported API set. Many core modules include a complete API listing:

When you encounter missing API on Android, you have options! You may be able to use alternate, supported APIs to accomplish the task. You can use Skip’s iOS and Android integration techniques to implement separate iOS and Android code paths, taking advantage of each platforms’ respective native solutions. And if the API you want to use is in a framework already mirrored for Android - either as a Skip open source library or a community library - you may be able to easily add the missing API to the existing library. If you augment an existing library, please consider contributing your improvements back to the Skip community. Follow the instructions here to configure Xcode for local Skip library development.

Frameworks

If you want to use an iOS framework that does not have a Skip or community library available, you might consider creating your own dual-platform library. Again, please consider contributing your work as a community library.

Features

Some iOS app extensions and features are not yet implemented for Android, or have no direct Android counterpart. Use the techniques in Platform Customization to implement iOS-only or Android-only solutions. For example, you might use a Skip compiler directive to exclude your iOS widget from transpilation, and include a Kotlin file implementing a native widget for Android instead.


Accessing Transpiled Output

The location of your transpiled output differs for app and framework targets. We discuss how to access your generated Kotlin code in Xcode below. The Platform Customization documentation covers how to work with your Android code in Android Studio.

Framework Transpilation Browse Screenshot

Apps

For apps, Xcode places Skip’s generated Kotlin in a plugins directory deep within Xcode’s DerivedData folder. Skip surfaces this directory as the SkipStone/plugins group in your Xcode project.

Transpiled source location

The first time you transpile - and each first re-transpile after deleting DerivedData - Xcode may not allow you to expand the SkipStone/plugins group. Restarting Xcode fixes the issue, but you can fix it without restarting by explicitly marking SkipStone/plugins as a folder. Highlight the SkipStone/plugins group, then use Xcode’s right panel to set the group Type to Folder.

Transpiled source location

Skip includes the Create SkipLink command plugin to link to the transpiled output of your framework targets in Xcode. To invoke the command, control-click your Skip-ified package or highlight the package and use the File -> Packages menu.

Framework SkipLink Screenshot

After running the Create SkipLink command, the new SkipLink Xcode group will allow you to access your framework’s generated Android project and Kotlin source.

Jumping to Kotlin Files

After linking to the transpiled Kotlin files using the instructions above, you can jump to Kotlin files using Xcode’s Open Quickly (cmd-shift-O) command, just as you can jump to Swift files. Transpiled Kotlin files have the same names as their Swift counterparts, but use the .kt file extension.

Keep in mind that in order to work around limitations on Kotlin extension properties and functions, Skip typically moves code from your Swift extensions into the declaring Kotlin class during transpilation. If you defined an extension in a separate file from its declaring type, you may find its transpilation in the Kotlin file of the declaring type rather than the Kotlin file for the extension.

You can also jump to the Kotlin file from an Xcode build error message. When the Kotlin compiler outputs an error message, Skip’s Xcode plugin surfaces it as an Xcode build error in both the Swift and Kotlin source files. Clicking the build error for the Kotlin file will jump to the offending Kotlin code.


Tips

  • Stay on the happy path. Many APIs are mirrored for Android, but others are still in progress. For your shared code, use known working components and APIs as documented in the individual Skip modules and previewed in the sample apps like Showcase. Experimentation and iteration is the best way to explore the ever-expanding boundaries of Skip’s Android implementations. And remember that you aren’t limited by what Skip currently supports. With Skip’s platform customization techniques, you can use any iOS code for your iOS app, and fall back to supported or Android-specific code for your Android version.
  • KYSS: Keep Your Swift Simple. In particular, avoid overly-complex uses of Swift generics, which can be impossible to translate to equivalent Kotlin generics. Familiarize yourself with the Swift Support Reference so that you know what Swift language features to avoid.
  • Re-run frequently. For app projects, re-run the app on both platforms constantly. Skip’s transpiler is designed to accommodate incremental development and enable the re-launching of an app on both iOS simulator and Android emulator in mere seconds. Only by re-running frequently will you be able to quickly identify and resolve platform-specific issues and API limitations before they accumulate.
  • Modularize your projects for faster build and test iteration cycles. See the Getting Started documentation for instructions on creating multi-module apps.
  • Re-test frequently. Dividing up your app into separately-testable component modules makes it easier to iterate on that part of an app. See Testing.