Platform Customization
Whether to work around Skip limitations, differentiate your iOS and Android experiences, or take advantage of platform-specific features, you will likely find yourself wanting to make parts of your app iOS-only or Android-only. Skip includes features that make this easy.
Compiler Directives
The most common and comprehensive mechanism for writing iOS or Android-only code is conditional compiler directives. You probably already use these in your code. 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
Important: Android-only code in these blocks is still parsed by Xcode, and it is still transpiled by Skip. So syntactically, it must be valid Swift code. But it is not compiled as Swift. In fact, it is invisible to the Swift compiler. This unlocks a superpower: the ability to directly use Kotlin and Java API.
Imagine, for example, that Skip did not support the iOS date formatting API. You could work around this limitations 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. When Skip uses bespoke types to represent common data structures, we have standardized on two simple methods for converting between these types and their Kotlin or Java 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 anArray
, Skip’s transpiled output usesskip.lib.Array
, a Skip type designed to mirror the API and value semantics of Swift arrays. CallingsomeArray.kotlin()
returns akotlin.collections.List
containing the same elements. By default, Skip always returns a copy, but you can use the optionalnocopy
parameter to hint that this is not necessary. In that case, Skip will return theArray
’s backingList
instance if possible..swift(nocopy: Bool = false)
: This function goes in the other direction, converting native Kotlin and Java types to their equivalents for use in your transpiled Swift code. CallingsomeList.swift()
on akotlin.collections.List
instance will return askip.lib.Array
.
Here is the actual implementation of these functions 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 {
public func kotlin(nocopy: Bool = false) -> java.util.Calendar {
return nocopy ? platformValue : platformValue.clone() as java.util.Calendar
}
}
extension java.util.Calendar {
public func swift(nocopy: Bool = false) -> Calendar {
let platformValue = nocopy ? self : clone() as java.util.Calendar
return Calendar(platformValue: platformValue)
}
}
#endif
The .kotlin()
and .swift()
functions are only needed when passing builtin types with different representations to and from Kotlin and Java API. They aren’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.
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 code.
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 only recognizes one attribute:nocopy
. See the Structs topic for an explanation.// 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 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. UnlikeSKIP 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 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.
SwiftUI and Compose
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:
- At this early stage in Skip’s development, many SwiftUI APIs aren’t yet implemented for Android. Being able to trivially mix Compose views into your UI enables you to work around any of Skip’s temporary shortcomings.
- Easily embed Android-specific UI components and access Android-specific features from anywhere in your app.
- If you’re a large team or just 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 include Kotlin files full of custom Kotlin and Compose functions that you call using these techniques.
See the ComposeView
topic in the SkipUI module for a discussion of embedding Compose calls in SwiftUI and vice versa.