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 or Android 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 is no exception. That is why we concentrated on making it trivial to exclude unsupported iOS code from your Android build. You can truly use any iOS features, right inline, without compromise and without re-architecting your app.

Of course, using an iOS feature without 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: 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.

Running on an Android Device

In order to be able to install and run an app on a connected Android device, you must enable USB debugging on the device, as per the ADB documentation. Then pair the Android device with your development machine.

Make sure only one device or emulator is running at a time. Otherwise Skip cannot know where to launch your app. Alternatively, set the ANDROID_SERIAL variable in your project’s .xcconfig file to the desired device or emulator’s identifier. Running the /opt/homebrew/bin/adb devices command will show the available paired identifiers.

There is often a significant difference between Debug and Release build performance on Android devices. Always use a Release build when evaluating real-world performance.

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

There is an incompatibility between the new Xcode Previews and Skip apps. Xcode no longer sets the appropriate environment variable to identify Preview builds, causing every update to attempt a full Android rebuild. This issue is discussed at https://github.com/orgs/skiptools/discussions/263. The workaround is to enable Editor > Canvas > Use Legacy Previews Execution. We have filed a bug with Apple, and we hope this workaround is unnecessary in the future.

Separate iOS and Android Apps

If you’ve chosen to create separate iOS and Android apps that share dual-platform Swift frameworks, then you will build and run each app in its respective IDE. The Getting Started guide contains tips for integrating dual-platform frameworks into your development workflow.

Frameworks

Building a dual-platform framework in Xcode builds your iOS code and runs the SkipStone build plugin. 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, or to export the framework’s build artifacts. For more information, see the testing and deployment documentation.

Framework Test Development Screenshot

You must run your tests against a macOS - not iOS - destination in order to perform an Android framework build.

iOS-Only

By default, whenever you run your iOS app from Xcode, Skip will also create and run the Android app. Building and running the app side-by-side is very useful for ensuring that both sides of the app look and behave the same while iterating on the app.

However, you may sometimes want to run only the iOS side of the app for certain time periods, such as when debugging an iOS-specific issue. To do this, you can edit the AppName.xcconfig file, and change the SKIP_ACTION = launch to SKIP_ACTION = build. This will cause the Android side of the app to be built, but not run.

If you want to skip over the entire Kotlin Gradle build process – which is not recommended, since it may result in transpilation errors accumulating without any indication, you can instead set the SKIP_ZERO = 1 property in the AppName.xcconfig file.


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. 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 dual-platform coverage.
  2. If you are writing a native module, our porting guide covers some of the common issues you’ll run into when compiling cross-platform Swift. Additionally, compiled Swift must use bridging to interact with Android’s Kotlin and Java APIs.
  3. If, on the other hand, you are writing a transpiled module, you may run into limitations in the Swift that can be transpiled. 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 the transpiler supports, see the Transpilation Reference.
  4. Writing a dual-platform apps means using dual-platform libraries. Our documentation on dependencies discusses how to use other dual-platform libraries as well as iOS and Android-specific libraries.

Native modules currently have the following additional limitations. We are working to lift these limitations in the near future:

  • UserDefaults is not implemented.
  • Resource loading through Bundle.module is not implemented.
  • NSLocalizedString is not implemented.

Because Skip uses transpilation for SwiftUI, these functions do work in your SwiftUI code.

Build Errors

Skip tries to warn you as quickly as possible when you’re going down the wrong path. For example, the Skip build plugin may report warnings and errors even before Skip attempts to compile your project for Android. Skip also attempts to map all errors back to the offending Swift source code and surface them in Xcode, whether they come from bridging, transpilation, Kotlin compilation, or native compilation. Each error message therefore 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.

When an error comes from compiling Kotlin, the Xcode sidebar will also include a message linking to the generated Kotlin source.

Framework Development Screenshot

The most common build 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.
  • When using native mode, needing to modify your imports for cross-platform Swift.
  • When using transpiled mode, 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.

Runtime Errors and Debugging

Dealing with errors is an integral part of development. Be sure to read the Debugging chapter to learn how to access generated code, view your log statements, and debug the Android side of your Skip framework or application.

Structs, Garbage Collection, Numeric Types, and More

Swift and Kotlin differ in more than just syntax. The Bridging Reference and Transpilation Reference cover 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 bridged or transpiled code will behave on Android and keep you from making many common mistakes.


UI Coding

Google recommends Jetpack Compose for Android user interface development. Skip can translate a large subset of SwiftUI into Compose, allowing you to build cross-platform iOS and Android UI in SwiftUI. Or you can write a separate Android UI in pure Compose using your Android IDE of choice. Skip even allows you to move fluidly between SwiftUI and Compose, as described in our platform customization documentation. In the end, the choice between using SwiftUI, Jetpack Compose, or a combination of the two is up to you.

@Observables

Regardless of whether you use Skip’s translated SwiftUI or write to the Compose API in Kotlin, Skip ensures that your @Observable model types participate in Compose state tracking. This allows them to seamlessly power your Android user interface just as they power your iOS one.

@Observable integration is transparent, but some caveats apply when you mix native and transpiled modes or use a Swift model to power a bespoke Compose UI:

  • If you use a native model layer, you must import SkipFuse in your @Observable's Swift file in order to fuse it to the Android runtime. The SkipStone build plugin will warn you if this import is missing.
  • In order to use a native @Observable from a Kotlin or transpiled UI, ensure that your native @Observable is bridged.
  • Finally, if you are writing a bespoke Compose UI, you must add a SwiftPM dependency on SkipModel for your @Observables to work properly on Android, as in the following example. This is not necessary when you use a SwiftUI interface, because SkipModel will be included in your dependency on SkipUI.
...
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", from: "1.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")]),
        ...
    ]
)

SwiftUI

Writing cross-platform SwiftUI and watching it appear on both the iOS simulator and Android emulator can feel magical. It is a wonderful way to share all or parts of your user interface. And because Skip translates your SwiftUI calls to Jetpack Compose on Android, the result is a fully native user interface on both platforms, not an uncanny-valley replica.

Skip supports both native and transpiled modes for SwiftUI modules. When using native mode, import SkipFuseUI rather than SwiftUI on Android, because Apple’s SwiftUI is of course not available. You must also use default or public visibility for Views and their SwiftUI properties on Android. Skip cannot access private SwiftUI components. Here is an example of a valid cross-platform SwiftUI view in native mode:

#if os(Android)
import SkipFuseUI
#else
import SkipUI
#endif 

struct MyView : View {
    @State var counter = 1 // Use internal or public access for all SwiftUI types and members
    private let title = "..." // OK to use private for non-SwiftUI members 
    ...

    var body: some View {
        ...
    } 
}

These restrictions do not apply to transpiled mode.

Using SwiftUI in native mode is a work in progress. We recommend using Skip’s transpiled mode for your UI until native mode is complete, after which you can migrate to native if desired.


iOS Features without Android Support

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

With Skip, you never have to compromise your iOS app. You can always use compiler directives to exclude unsupported iOS code from your Android build. 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 errors from the Android Swift or Kotlin compilers. These errors will come at build time.

If you are writing native Swift, the SkipFuse module documentation includes important information about fusing the Swift and Android worlds.

If you are writing transpiled Swift, each of Skip’s transpiled core modules specifies its current status and Android-supported API set:

If you are writing SwiftUI code - whether in native or transpiled mode - consult the SkipUI module to learn what is supported.

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 or shared API. 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 compiler directive to exclude your iOS widget from your Android build, and include a Kotlin file implementing a native widget for Android instead.


Common Topics

For instructions on how to handle common development tasks like localization, resource and image loading, and JSON coding across platforms with Skip, see Development Topics.


Tips

  • Stay on the happy path. Many APIs are available for Android, but others are still in progress. For your shared code, use known working components and APIs as 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.
  • Re-run frequently. For app projects, re-run the app on both platforms constantly. Skip’s tools are 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.
  • KYSS: When using transpiled mode, 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 Transpilation Reference so that you know what Swift language features to avoid. This does not apply in native mode, where the full Swift language is available.