Menu

Platform Customization

Whether to work around limitations in Skip’s Android support, differentiate your iOS and Android experiences, take advantage of OS-specific features, or simply because you prefer to write parts of your app separately for each platform, you will likely find yourself wanting to write iOS-only or Android-only code. Skip makes this easy.

Compiler Directives

The most common and convenient mechanism for writing iOS or Android-only code is conditional compiler directives. You probably already use these in your Swift. For example:

#if DEBUG
...
#else
...
#endif

Like the Swift compiler, the Skip transpiler can conditionally include or exclude code based on these conditionals. By default, Skip only recognizes two symbols as evaluating to true: SKIP or os(Android). The Swift compiler, meanwhile, considers these symbols false. You can use them, therefore, to create blocks of code that are only compiled into your iOS app or only transpiled into your Android app.

The following code blocks would all print “Android” on Android and “iOS” on iOS:

#if SKIP
print("Android")
#endif

#if !SKIP
print("iOS")
#endif

    
#if SKIP
print("Android")
#else
print("iOS")
#endif

    
#if os(Android)
print("Android")
#else
print("iOS")
#endif

You can also use compiler directives in SwiftUI modifier chains:

Text("Hello World")
    #if SKIP
    .italic()
    #else
    .bold()
    #endif

Calling Kotlin and Java API

These compiler directives unlock a Skip superpower: the ability to directly use Kotlin and Java API. Android-only code in #if SKIP blocks is still parsed by Xcode, so it must have valid Swift syntax. But it is excluded from your iOS build and invisible to the Swift compiler, which allows it to make any syntactically valid API call without causing Xcode errors. Your Swift can call Kotlin and Java Android API just as if you were writing in Kotlin, because Skip’s transpilation strategy means you are effectively writing in Kotlin.

Imagine, for example, that Skip did not support the iOS date formatting API. You could work around this limitation with code like the following:

#if SKIP
let dateFormat = java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", java.util.Locale.getDefault())
dateFormat.timeZone = java.util.TimeZone.getTimeZone("GMT")
return dateFormat.format(java.util.Date())
#else
... equivalent iOS code ...
#endif

While this example uses fully-qualified Java type names, that is not required. Skip allows you to import Kotlin and Java packages so that you can use unqualified names. See the documentation on dependencies.

Of course, you aren’t limited to simple tasks like date formatting. When interfacing with more involved Kotlin and Java API, you may need to pass complex data back and forth. This is typically easy, because the Skip transpiler unifies the Swift and Kotlin type systems. When Skip does use bespoke types to represent common data structures, though, we have standardized on a simple method for converting these types to their Kotlin equivalents:

  • .kotlin(nocopy: Bool = false): Skip implements this function for its Swift types on Android to return the equivalent standard Kotlin object. For example, when your Swift code uses an Array, Skip’s transpiled output uses skip.lib.Array, a Skip type designed to mirror the API and value semantics of Swift arrays. Calling someArray.kotlin() returns a kotlin.collections.List containing the same elements. By default, Skip returns a copy which also recursively invokes .kotlin() on each element. You can use the optional nocopy parameter to hint that this is not necessary. In that case, Skip will return the Array’s backing List instance directly if possible.

Here is the actual implementation of this function for Calendar in the SkipFoundation package. Note that SkipFoundation’s Calendar uses an internal platformValue member of type java.util.Calendar to take advantage of Java’s existing calendaring functionality.

#if SKIP
extension Calendar: KotlinConverting<java.util.Calendar> {
    public func kotlin(nocopy: Bool = false) -> java.util.Calendar {
        return nocopy ? platformValue : platformValue.clone() as java.util.Calendar
    }
}
#endif

Again, the .kotlin() function is only needed when passing builtin types with different representations to Kotlin and Java API. It isn’t needed for basic types like Int or String, and types that you define yourself can directly implement Kotlin or Java protocols for interfacing with Android API.

Types that provide custom .kotlin() conversion functions also provide a constructor that accepts their Kotlin form, should you need to convert from the Kotlin type back to the Swift equivalent. To build on the example above, SkipFoundation’s Calendar has an init(platformValue: java.util.Calendar) constructor.

The ability to interact so easily with Kotlin and Java APIs is powerful. Later, we’ll see how to take advantage of it to mix SwiftUI and Compose views.

Syntax for Calling Kotlin and Java

When you call Kotlin and Java code from within #if SKIP blocks, remember that you are still writing in Swift. Your code will be syntax-checked by Xcode, and it will be parsed and transpiled by Skip. Just write natural Swift code, and imagine that the Kotlin or Java API you’re calling is a Swift library API.

If you’re attempting to cut and paste Kotlin inline, you’ll have to turn it into valid Swift to avoid syntax errors. Luckily, the languages are extremely similar. You’ll typically only have to change a few calling conventions:

  • Named parameter values in Kotlin are specified with an equals (=) sign, whereas in Swift they use a colon (:). Note that parameter names are never required in Kotlin.
  • Closure arguments in Kotlin are specified by an arrow (->), whereas in Swift they use the in keyword.
  • To import everything in a Kotlin package, use import com.xyz.__ rather than import com.xyz.*. The latter is not valid Swift.

For example, the following Kotlin:

val start = 1
var result = 0
for (i in 1 until 10) { 
    result += someFunction(value = start + i, block = { arg -> arg + 1 })
}

Would use the following syntax when embedded in a Swift #if SKIP block:

let start = 1
var result = 0
for i in 1..<10 {
    result += someFunction(value: start + i, block: { arg in arg + 1 })
}

Skip Comments

Skip treats any // or /* */ comment line beginning with “SKIP” as a transpiler instruction. Skip supports several instructions, all of which influence the transpiler’s output or behavior.

  • SKIP ATTRIBUTES: <Attributes> This instruction applies Skip-specific attributes to the target element. Skip currently recognizes two attributes: nocopy and nodispatch. See the Structs topic for an explanation of nocopy. Use nodispatch on an async function to prevent Skip from inserting Kotlin to run it on a Dispatcher.

      // SKIP ATTRIBUTES: nocopy
      struct S {
          ...
      }
    
  • SKIP DECLARE: <Kotlin> Replace the target declaration with custom Kotlin. This is useful to customize how a type, function, or property is declared in Kotlin, without affecting the transpilation of its body.

      // SKIP DECLARE: override fun SaverScope.save(value: Any): Any?
      override func save(value: Any) -> Any? {
          ...
      }
    
  • SKIP EXTERN: Denote an external native function. This instruction is used in C integration.
  • SKIP INSERT: <Kotlin> Insert arbitrary Kotlin.

      // SKIP INSERT: var count by remember { mutableStateOf(100) }
      var countString = count.description
    
  • SKIP NOWARN Place this comment on the offending line to silence a Skip warning or error.

      // SKIP NOWARN
      let dict = obj as? Dictionary<Int, String>
    
  • SKIP REPLACE: <Kotlin> Replace the target statement with arbitrary Kotlin. Unlike SKIP DECLARE, this includes the body.

      // SKIP REPLACE:
      // fun printOS() {
      //     print("Android")
      // }
      public func printOS() {
          print("iOS")
      }
    
  • SKIP SYMBOLFILE This instruction is used at the top of a Swift source file. It tells Skip that the file should only be used to gather symbols about declared API. Skip expects that you will implement the API with a corresponding Kotlin file. In effect, it turns the Swift source into a header file. SkipLib uses this technique extensively to implement aspects of the Swift standard library in Kotlin.

Instructions that specify Kotlin code can span multiple comment lines. Skip assumes that the instruction’s Kotlin continues until Skip sees one of:

  • A new instruction
  • A blank comment line
  • The end of the comment block

Thus, this:

// SKIP INSERT: var count by remember { mutableStateOf(100) }

And this are both valid:

// SKIP INSERT: 
// var count by remember {
//     mutableStateOf(100)
// }

Kotlin Files

The mechanisms covered so far allow you to use Kotlin API from Swift or to embed bits of pure Kotlin within your Swift source. But Skip also allows you to include entire Kotlin source files in your Xcode project. Simply use the standard .kt extension and place your Kotlin source files into the Sources/<ModuleName>/Skip directory alongside your skip.yml file. Skip will include them in the resulting Android build. SkipLib uses this technique to implement aspects of the Swift standard library in Kotlin.

When working on Kotlin files, we typically prefer to open the generated Android project in Android Studio to take advantage of its Kotlin syntax highlighting, autocompletion, and other niceties. In fact, you could have a dedicated Android team working to extend and customize the Android version of your app using Kotlin and Android Studio, while other teams work on the iOS and shared portions of the codebase.

The package name of your Kotlin files included in the Skip/ folder must match the derived (de-camel-cased) package name from the Swift module. For example, if you are including the file Sources/MyModule/Skip/CustomKotlin.kt, the package header should be package my.module.


Working in Android Studio

Skip generates a complete gradle project from your dual-platform app or framework. You can open this project in Android Studio to debug Android-specific issues and unit tests, iterate on your Kotlin files, or prototype solutions that you back-port to inline Swift using compiler directives.

Apps

  1. Make sure that you have built your project in Xcode.
  2. Control-click the Android/settings.gradle.kts file and select Open with External Editor from the resulting context menu.

Open in Android Studio

To run your unit tests in Android Studio, first run them in Xcode so that they get transpiled. Then find your Test module’s output folder within SkipStone/plugins, and use Open with External Editor on its settings.gradle.kts file. Running your unit tests in Android Studio will allow you to use Android’s native debugging tools to debug your tests as well.

Open in Android Studio

Attempting to run your app in Android Studio may result in an “SDK not found” error. If you receive this error simply copy your Xcode project’s Android/local.properties file to the path given in the error message.

Frameworks

  1. Make sure that you have built your project in Xcode.
  2. Follow the instructions to create the SkipLink Xcode group.
  3. Control-click the Android/settings.gradle.kts file and select Open with External Editor from the resulting context menu.

To run your unit tests in Android Studio, first run them in Xcode so that they get transpiled. They will then be available in the gradle project. Running your unit tests in Android Studio will allow you to use Android’s native debugging tools to debug your tests as well.


Compose Integration

Skip’s SwiftUI implementation includes additional API that allows you to move fluidly between SwiftUI and pure Compose code. This powerful capability is useful for several reasons:

  • If you prefer to code portions of your app entirely in Compose, Skip’s interoperability allows you to share as much or as little of your UI as you like between the platforms, without worrying about fighting the framework. You can add Kotlin libraries and include Kotlin files full of custom Kotlin and Compose functions that you call using the techniques in this chapter.
  • Easily embed Android-specific UI components and access Android-specific features from anywhere in your app.
  • Skip hasn’t yet implemented every SwiftUI API for Android. Being able to trivially mix Compose views into your UI enables you to work around any of Skip’s temporary shortcomings.

See the ComposeView topic in the SkipUI module for a discussion of embedding Compose calls in SwiftUI and vice versa.


Model Integration

Skip ensures that the @Observable and ObservableObject types you define in your shared Swift business logic can seamlessly power your Compose UI as well. The SkipModel module describes Skip’s dual-platform model object support.

Skip’s AsyncStream implementations also feature deep Kotlin integration. You can construct an AsyncStream from a Kotlin Flow, and you can retrieve a Flow from an AsyncStream using the standard .kotlin() function.

These model integrations are additional ways that Skip allows you to move seamlessly between shared and platform-specific code.