Transpilation Reference
Skip’s Swift to Kotlin language transpiler is able to convert a large subset of the Swift language into Kotlin. The result is what we call Kotlish: code that is syntactically Swift but compiles and runs as Kotlin on Android. The transpiler has the following goals:
- Avoid generating buggy code. We would rather give you an immediate error or generate Kotlin that fails to compile altogether than to generate Kotlin that compiles but behaves differently than your Swift source.
- Allow you to write natural Swift. Swift is a sprawling language; we attempt to supports its most common and useful features so that you can code with confidence.
- Generate idiomatic Kotlin. Where possible, we strive to generate clean and idiomatic Kotlin from your Swift source.
These goals form a hierarchy. For example, if generating more idiomatic Kotlin would run the risk of introducing subtle behavioral differences from the source Swift, Skip will always opt for a less idiomatic but bug-free transpilation.
Language Features
Section titled “Language Features”The following table details Skip’s support for transpiling various Swift language features. A ✓ indicates that a feature is fully or very strongly supported. A ~ indicates that a feature is partially supported. And a ✕ indicates that a feature is not supported, or is only weakly supported. Future releases may address some unsupported language features, but others reflect deep incompatibilities between the Swift and Kotlin languages.
- ✓ Classes
- ✓ Inheritance
- ✓
Codablesynthesis
- ✓ Structs
- ✓ Value semantics. See the Structs topic below
- ✓ Constructor synthesis
- ✓
Equatablesynthesis - ✓
Hashablesynthesis - ✓
Codablesynthesis
- ✓ Protocols
- ✓ Enums
- ✓ Enums with associated values
- ✓
RawRepresentablesynthesis - ✓
CaseIterablesynthesis - ✓
Equatablesynthesis - ✓
Hashablesynthesis - ~
Codablesynthesis- Skip can only synthesize
Codableconformance forRawRepresentableenums
- Skip can only synthesize
- ✓ Nested types
- ✓ Types defined within types
- ✕ Types defined within functions
- ✓ Extensions
- ✓ Concrete type extensions
- ✓ Protocol extensions
- ~ Limits on generic specialization
- ~ Limits on extending types defined in other modules
- ✓ Generic types
- ~ See the Generics topic below for limitations
- ✓ Tuples
- ✓ Labeled or unlabeled
- ✓ Destructuring
- ✓ Arity 2 through 5
- ✕ Arity 6+
- ✓ Typealiases
- ✓ Nested typealiases
- Skip fully resolves typealiases during transpilation to work around Kotlin typealias limitations
- ✓ Nested typealiases
- ✓ Properties
- ✓
let - ✓
var - ✓ Static properties
- ✓ Stored properties
- ✓ Computed properties
- ✓ Throwing properties
- ✓ Lazy properties
- ✓ Custom get/set
- ✓
willSet - ✓
didSet - ✓ SwiftUI property wrappers:
@State,@Environment, etc - ✕ Custom property wrappers
- ✓
- ✓ Functions
- ✓ Overloading on types
- ✓ Overloading on param labels
- ✕ Overloading on return type
- ✓ Static functions
- ✓ Generic functions
- ✓ Throwing functions
- ✓
selfassignment in mutable functions - ✓ Default parameter values
- ✓
inoutparameters - ✓ Closures and trailing closures
- ✓ Variadic parameters
- ✕
@autoclosureparameters - ✕ Parameter packs
- ✓ Nested functions
- ✓ Constructors
- ✓ Optional constructors
- ✓
selfassignment in constructors - ~ Kotlin imposes some limitations on calling
super.initorself.initin a delegating constructor - ✕ Constructors cannot use generic parameter types that are not declared by the owning type
- ✓ Deconstructors
- ~
deinitis transpiled into Kotlin’sfinalize. See the Garbage Collection topic
- ~
- ✓ Closures
- ✓ Explicit and implicit (
$0,$1, etc) parameters - ~ Weak and unowned capture is ignored. We rely on Kotlin garbage collection
- ✓ Explicit and implicit (
- ✓ Error handling
- ✓
throw - ✓
do / catch - ✓
try, try?, try! - ✓ Throw custom enums, structs, classes
- ✓ Catch pattern matching
- ✕ Error types cannot be subclasses
- ✓
- ✓ Concurrency
- ✓
Task/Task.detached - ✓ Task groups
- ✓
async / await - ~
async let- The implicit task group is not cancelled when exiting scope
- ✓ Async functions
- ✓ Async properties
- ✓ Async closures
- ✓
AsyncSequence - ✓
AsyncStream - ✓
@MainActor - ~ Custom actors
- Non-private mutable properties not supported. Expose functions to access private state
- ✕ Grand Central Dispatch
- ✓
- ✓ Defer
- ✓ If
- ✓
if let- See the If let topic for additional information
- ✓
if case
- ✓
- ✓ Guard
- ✓
guard let- See the If let topic for additional information
- ✓
guard case
- ✓
- ✓ Switch
- ✓ Case pattern matching
- ✓ Case binding
- ~ Limits on partial matching and binding
- ✕
case … where
- ✓ While loop
- ✓ Do while loop
- ✓ For in loop
- ✓
for … in … where … - ✓
for let … - ✓
for case …
- ✓
- ✓ Operators
- ✓ Standard operators
- ✓ Logical operators
- ✓ Optional chaining
- ✓ Optional unwrapping
- ✓ Range operators
- ~ Slice operators
- Slices are not mutable
- ~ Some advanced operators not supported
- ✓ Custom
Equatablewith== - ✓ Custom
Hashablewithhash(into:) - ✓ Custom
Comparablewith< - ~ Custom subscript operators
- Cannot overload subscript operators on parameter labels or types
- ✓
callAsFunctionsupport - ✕ Other custom operators
- ~ Key paths
- ✓ As implicit closure parameters
- ✓ As
@Environmentkeys - ✕ Other uses
- ✕ Macros
- ✓
@Observable - ✓
@ObservationIgnored - ✕ Other macros
- ✓
Builtin Types
Section titled “Builtin Types”The following table details Skip’s support for using builtin Swift standard library types in transpiled code. Support for these types is divided between the Skip language transpiler and the SkipLib open source library.
- ✓ Numeric types
- ✓ Use Kotlin native types
- ~
Intis 32 bit on JVM - ~ All unsigned and
Floatvalues must be explicit - e.g.Float(1.0); no implicit conversion from signed types orDouble
- ✓
String- ✓ Uses Kotlin native
String - ✕ Mutation is not supported
- ✓ Uses Kotlin native
- ✓
Any,AnyObject - ✓ Optionals
- ✕ Kotlin does not represent
Optionalas its own type, so.someand.nonedo not exist
- ✕ Kotlin does not represent
- ✕ Compound types (e.g.
A & B) - ✓
Array- ✓ Value semantics
- ✓ Slicing
- ✓
Dictionary- ✓ Value semantics
- ✓
Set- ✓ Value semantics
- ✓
OptionSet- ~ You must implement
OptionSetwith astruct
- ~ You must implement
- ✓
CaseIterable- ✓ Automatic synthesis
- ✓ Custom implementations
- ✓
Codable- ✓ Automatic synthesis
- ✓ Custom implementations
- ✓
CustomStringConvertible - ✓
Comparable- ✓ Automatic synthesis
- ✓ Custom implementations
- ✓
Equatable- ✓ Automatic synthesis
- ✓ Custom implementations
- ✓
Error - ✓
Hashable- ✓ Automatic synthesis
- ✓ Custom implementations
- ✓
RawRepresentable- ✓ Automatic synthesis
- ✓ Custom implementations
- ✓ `Result“
- ~ Result builders
- ✓
@ViewBuilder- The
@ViewBuilderattribute is not inherited when overriding API other thanView.body. Specify it explicitly
- The
- ✕ Other result builders
- ✓
The Kotlish Dialect
Section titled “The Kotlish Dialect”Kotlish is the informal name for the hybrid dialect that exists at the intersection of Swift and Kotlin. It is code that lives in Swift source files, is parsed by Xcode as valid Swift, is ignored by the Swift compiler, and is then transpiled by the skipstone plugin into Kotlin. The result is a kind of pidgin language: syntactically Swift, semantically Kotlin, and surprisingly practical given its accidental design.
Swift and Kotlin are undeniably similar. They were both designed in the 2010s as modern replacements for aging platform languages (Objective-C and Java, respectively), and they arrived at many of the same conclusions: type inference, null safety, trailing closures, value types vs. reference types, first-class functions, and pattern matching, among others. The resemblance is close enough that a substantial amount of code is valid in both languages with only minor syntactic adjustments.
Skip’s transpiler takes advantage of this resemblance. When you write code inside a #if SKIP block, you are writing Swift syntax that Skip converts to Kotlin. Because Xcode sees the code but the Swift compiler skips it (the block is conditionally excluded from the iOS build), you can reference Kotlin and Java types freely. Xcode will not complain about undefined symbols because it never tries to compile the block. And because Swift and Kotlin are so structurally alike, the transpiler’s job is often straightforward: rename let to val, swap : for = in named arguments, replace in with -> in closures, and the rest falls into place.
This is Kotlish. It is not a language specification. It is not versioned. It is a happy accident of convergent language design, formalized just enough by Skip’s transpiler to be genuinely useful.
Kotlish Syntax
Section titled “Kotlish Syntax”Kotlish lives inside #if SKIP blocks. Everything inside these blocks must be syntactically valid Swift, because Xcode’s parser still reads it. But it does not need to be semantically valid Swift, because the Swift compiler never evaluates it. This enables a lot of freedom and flexibility.
Swift Syntax Constraints
Section titled “Swift Syntax Constraints”Even though the code will become Kotlin, you must follow Swift’s grammatical rules. The most common tripping points when translating Kotlin snippets into Kotlish:
Named parameters use a colon in Swift, not an equals sign:
// KotlinsomeFunction(value = 42, label = "hello")
// KotlishsomeFunction(value: 42, label: "hello")Closure arguments use the in keyword, not an arrow:
// Kotlinlist.map { item -> item.name }
// Kotlishlist.map { item in item.name }Wildcard imports use .__ instead of .*, because .* is not valid Swift:
// Kotlinimport com.google.maps.android.compose.*
// Kotlishimport com.google.maps.android.compose.__Variable declarations use let and var, not val and var:
// Kotlinval name = "Skip"
// Kotlishlet name = "Skip"Kotlish Limitations
Section titled “Kotlish Limitations”Some Kotlin constructs have no syntactic equivalent in Swift. The most notable offender is the :: scope resolution operator, used in Kotlin for class references and member references:
// Valid Kotlin, invalid Swift@OptIn(DelicateCoroutinesApi::class)val ref = String::lengthThere is no way to write :: in Swift source and have Xcode accept it. For these situations, you need an escape hatch. Skip provides several, and they are covered in Escape Hatches below.
Other constructs that require workarounds:
- Kotlin
whenexpressions. Swift usesswitch, and while Skip transpilesswitchtowhen, some Kotlinwhenpatterns do not map cleanly back toswitch. - Kotlin
companion objectdeclarations. Skip handles static members automatically, but explicitcompanion objectblocks cannot be written in Swift syntax. - Java-style annotations with arguments. Some annotations like
@SuppressWarnings("unchecked")can be written in Kotlish, but complex annotation syntax may requireSKIP INSERT. - Kotlin extension properties with custom getters. Swift extensions support computed properties, so these usually work, but the syntax can diverge for more exotic patterns.
Escape Hatches
Section titled “Escape Hatches”When Kotlish reaches its limits, Skip provides a set of comment directives that let you inject, replace, or override pieces of Kotlin directly. These are not hacks; they are a deliberate part of Skip’s design, acknowledged in the Cross-Platform Topics documentation. Think of them as the emergency exits in an otherwise well-organized building.
SKIP INSERT
Section titled “SKIP INSERT”Inserts raw Kotlin into the transpiler output. The Swift code that follows it (if any) is also transpiled normally. Use this when you need to add something that has no Swift equivalent at all.
// SKIP INSERT: @OptIn(DelicateCoroutinesApi::class)func post(_ notification: Notification) { // Swift implementation...}The @OptIn(DelicateCoroutinesApi::class) annotation contains the forbidden :: operator, so it cannot exist as Swift source. The SKIP INSERT comment injects it directly into the Kotlin output, and the function declaration beneath it is transpiled normally.
You can also use SKIP INSERT to define entirely new Kotlin constructs that have no Swift counterpart:
// SKIP INSERT: fun <T> Array(data: Data): Array<T> = data.bytes as Array<T>Multi-line insertions work by continuing across comment lines:
// SKIP INSERT:// var count by remember {// mutableStateOf(100)// }SKIP REPLACE
Section titled “SKIP REPLACE”Replaces the immediately following Swift statement with literal Kotlin. The Swift code is discarded entirely in the transpiled output. Use this when the transpiler produces incorrect Kotlin for a particular statement.
// SKIP REPLACE: mac.init(secretKeySpec)mac.init(secretKeySpec)Why would you need this? In this case, the transpiler sees .init( and assumes it is a constructor call, because that is what .init means in Swift. But in the Java Mac API, init is a regular method. The SKIP REPLACE directive tells the transpiler: “I know what I want here, just emit it verbatim.”
A multi-line replacement for an entire function:
// SKIP REPLACE:// fun printOS() {// print("Android")// }public func printOS() { print("iOS")}On iOS, printOS() prints “iOS”. On Android, the transpiler ignores the Swift function body and emits the Kotlin version from the comment, which prints “Android”.
SKIP DECLARE
Section titled “SKIP DECLARE”Replaces only the declaration of a type, function, or property, but keeps the transpiled body. This is the surgical option: you control how the thing is declared in Kotlin, but you let the transpiler handle the implementation.
// SKIP DECLARE: open class JSONEncoder: TopLevelEncoder<Data>open class JSONEncoder { // The body is transpiled normally...}Here, the Swift declaration open class JSONEncoder would transpile to a class with no supertype. The SKIP DECLARE directive overrides just the class header to add : TopLevelEncoder<Data>, while all the methods and properties inside are still transpiled from Swift.
SKIP NOWARN
Section titled “SKIP NOWARN”Silences a Skip transpiler warning or error on the following line. This is the “I know what I’m doing” directive:
// SKIP NOWARNlet dict = obj as? Dictionary<Int, String>Use it sparingly. If you find yourself sprinkling SKIP NOWARN everywhere, it usually means the approach needs rethinking rather than silencing.
SKIP SYMBOLFILE
Section titled “SKIP SYMBOLFILE”Marks an entire Swift source file as a header. Skip reads the file to gather type and function signatures, but it does not transpile the bodies. You are expected to provide the actual implementation in a Kotlin file. SkipLib uses this technique to implement parts of the Swift standard library in hand-written Kotlin.
SKIP EXTERN
Section titled “SKIP EXTERN”Marks a function as external in the Kotlin output, indicating that its implementation is provided by native code (typically via JNI). This is used in C and C++ integration scenarios.
Common Kotlish Patterns
Section titled “Common Kotlish Patterns”Over time, certain idioms have emerged in the Skip framework libraries that represent Kotlish at its most fluent. These patterns are worth studying if you are writing transpiled code that interacts heavily with Kotlin or Java APIs.
The Platform Value Wrapper
Section titled “The Platform Value Wrapper”Skip’s foundation libraries use a recurring pattern where a Swift struct wraps a “platform value,” which is the underlying Kotlin or Java type that does the real work:
#if SKIPpublic struct Date: KotlinConverting<java.util.Date> { internal var platformValue: java.util.Date
public init(platformValue: java.util.Date) { self.platformValue = platformValue }
public init(timeIntervalSince1970: TimeInterval) { self.platformValue = java.util.Date((timeIntervalSince1970 * 1000.0).toLong()) }
public func kotlin(nocopy: Bool = false) -> java.util.Date { return nocopy ? platformValue : platformValue.clone() as java.util.Date }}#endifThe Swift API surface (Date, timeIntervalSince1970) is familiar to iOS developers. The implementation delegates to java.util.Date. The .kotlin() method provides explicit conversion when you need to hand the underlying Java object to a Kotlin API. See the Cross-Platform Topics documentation for more on this pattern.
Kotlin Type Conversions
Section titled “Kotlin Type Conversions”Kotlin and Java have explicit numeric conversion methods (.toLong(), .toDouble(), .toByte(), .toInt()) that do not exist in Swift’s standard library. Inside #if SKIP blocks, you can call these freely because the Swift compiler never evaluates the code:
#if SKIPlet millis = (interval * 1000.0).toLong()let bytes = array.map { $0.toUByte() }let hexString = java.lang.Byte.toUnsignedInt(b).toString(radix: 16).padStart(2, "0".get(0))#endifThis code looks somewhat alien to a Swift developer, but it is perfectly natural Kotlin. The transpiler passes these method calls through unchanged.
Direct Android API Calls
Section titled “Direct Android API Calls”The real power of Kotlish is calling Android platform APIs without any bridging layer:
#if SKIPimport android.app.NotificationChannelimport android.app.NotificationManagerimport android.content.Context
func createNotificationChannel() { let context = ProcessInfo.processInfo.androidContext let channel = NotificationChannel( "default", "Default", NotificationManager.IMPORTANCE_DEFAULT ) let manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager manager.createNotificationChannel(channel)}#endifThis function calls the Android notification API directly. There is no bridge, no wrapper, no generated binding. The Swift syntax is transpiled to Kotlin, and the Kotlin calls the Android SDK as normal.
Compose Integration
Section titled “Compose Integration”Kotlish is also how you embed Jetpack Compose views within a Skip app. The @Composable annotation, remember, mutableStateOf, and other Compose primitives are all accessible:
#if SKIPimport androidx.compose.runtime.__
struct MapComposer: ContentComposer { let latitude: Double let longitude: Double
@Composable func Compose(context: ComposeContext) { GoogleMap(cameraPositionState: rememberCameraPositionState { position = CameraPosition.fromLatLngZoom( LatLng(latitude, longitude), Float(12.0) ) }) }}#endifFor a full treatment of Compose integration, see the Compose Integration section of the Cross-Platform Topics documentation.
When to Use Kotlish
Section titled “When to Use Kotlish”Kotlish is the right tool when you need to:
- Call Android or Java APIs directly from a transpiled module.
- Integrate with third-party Kotlin libraries.
- Implement platform-specific functionality that Skip’s framework libraries do not yet cover.
- Write custom Compose views.
Kotlish is the wrong tool when:
- You are writing a Skip Fuse (native) module. In native mode, your Swift is compiled by the real Swift compiler, not transpiled. Use
#if os(Android)with AnyDynamicObject or bridged transpiled modules instead. - The code is getting unwieldy. If your
#if SKIPblock is growing into hundreds of lines of Kotlin-flavored Swift withSKIP REPLACEcomments every few lines, consider writing a Kotlin file instead. Android Studio will give you syntax highlighting, autocompletion, and compiler errors, none of which you get for Kotlish in Xcode. - You want type safety. Xcode cannot check Kotlish for semantic correctness. You will not discover type errors, missing methods, or wrong argument types until the Android build runs. For substantial Android-specific logic, a dedicated Kotlin file compiled by the Kotlin compiler is safer.
The Transpilation Reference
Section titled “The Transpilation Reference”For a comprehensive list of which Swift language features are supported by Skip’s transpiler, and how they map to Kotlin, see the Transpilation Reference. That document covers the full breadth of the transpiler’s capabilities, including edge cases around generics, enums, structs, concurrency, and more.
For the complete list of Skip comment directives and their syntax, see Skip Comments in the Cross-Platform Topics documentation.
Special Topics
Section titled “Special Topics”The Skip transpiler performs a large number of interesting code transformations to bridge the differences between Swift and Kotlin. The following sections address particular areas that deserve some explanation, either because the transpilation affects the behavior of your code, or because the resulting Kotlin is unusual in some way.
Numeric Types
Section titled “Numeric Types”Numeric types are a particularly common source of subtle runtime and compilation problems in dual-platform apps. Runtime issues may arise because Kotlin Ints are 32 bits. Technically Swift Ints can be either 32 or 64 bits depending on the hardware, but all of Apple’s recent devices are 64 bit, so Swift programmers tend to assume 64 bit integers. Take care to use Int64 when your code demands more than 32 bit integer values. In Java, overflowing the 32 bit range does not cause an error condition like in Swift, but instead silents wraps Int.max around to Int.min, making such issues a potential cause of hidden bugs.
You may also experience Android compilation problems because Kotlin can be picky about converting between numeric types. In general, you should be explicit when using any types other than Int and Double. For example, if var f is a Float, write f = Float(1.0) rather than f = 1.0. Also, although Int and Double do not need explicit casts, Kotlin does not allow you to assign an integer literal to a double variable or parameter. For example, if var d is a Double, Kotlin requires you to write d = 1.0 rather than d = 1. Skip attempts to convert your integer literals to decimals when needed, but there may be times when you’ll have to write your Double values as 1.0 rather than 1.
Other Primitive Types
Section titled “Other Primitive Types”Skip does not wrap Kotlin’s primitive types. We have chosen the massive efficiency and interoperability wins that come with using Kotlin’s primitive types directly over the additional Swift language compatibility we might be able to achieve if we wrapped Kotlin’s primitives in our own classes.
This means that we have to live with Kotlin’s primitive types as-is, and they have some limitations that will impact your code. The most significant is that these types are immutable. Functions like Bool.toggle() are not supported, and Strings are immutable in Skip code. Rather than appending to a String in place, you must create a new string. Rather than calling String.sort(), you must call let sorted = string.sorted(), etc. Additionally, Strings are not Collections. While we have added the Collection API to String, there is no way to add a new protocol to an existing Kotlin type. So while you can make all the Collection API calls you’re used to, you cannot pass a String to code that expects a Collection<Character>.
Garbage Collection
Section titled “Garbage Collection”Swift uses automatic reference counting to determine when to free memory for an object. Kotlin uses garbage collection. This difference has important consequences that you should keep in mind:
- On Android, your
deinitfunctions will be called at an indeterminate time, and may not be called at all. While Swift callsdeinitfunctions and deallocates memory as soon as an object’s reference count reaches zero, the timing of these tasks on Android is entirely at the discretion of the garbage collector. - The Android garbage collector can detect and cleanup reference cycles. In Swift, the most common uses of the
weakandunownedmodifiers are to avoid strong reference cycles. This is not a problem in Kotlin, and Kotlin therefore does not have these modifiers. Skip has chosen to ignoreweakandunownedmodifiers on properties and in closure capture lists, relying on the garbage collector instead. If you were planning to use aweakorunownedreference for reasons other than avoiding a strong reference cycle, you should consider alternatives.
Structs
Section titled “Structs”All Kotlin objects are reference types. Apart from primitives like Int, there are no value types. In order to allow you to use Swift structs but ensure identical behavior in your Android programs, Skip employs its own MutableStruct protocol.
Skip automatically adds the MutableStruct protocol to all mutable struct types. It uses the functions of this protocol to give Kotlin classes value semantics. You will notice this when you examine any Kotlin transpiled from Swift that uses mutable struct types:
- The Kotlin classes for your mutable struct types will adopt the
MutableStructprotocol and implement its required functions. - You will see calls to
.sref()sprinkled throughout your code. This stands for struct reference. Skip addssref()calls when it is necessary to copy Kotlin objects representing structs in order to maintain value semantics - e.g. when assigning a struct to a variable. - Properties that hold mutable struct types will gain custom getter and setter code to copy the value on the way in and out as needed.
- Functions that return mutable struct types will
sref()the value being returned.
While modern virtual machines are very good at managing large numbers of objects, in extreme cases you might want to modify your code to avoid excessive copying. We recommend that you do not worry about it until you see a performance problem.
For cases in which a struct is technically mutable but is never modified after you set its properties once - i.e. a configuration object - add the @nocopy attribute. This instructs Skip to treat the struct as immutable and avoid copying.
// SKIP @nocopystruct S { …}Generics
Section titled “Generics”There’s no getting around it: Swift generics are complicated. And converting from Swift generics to Kotlin generics is even more so, because the two languages have very different generic implementation strategies. Swift generics are built deep into the language as first-class citizens of its type system. Kotlin generics, on the other hand, don’t exist at the JVM level and are only present at compile time.
This difference has far-reaching effects. For example, because generics are built into Swift’s type system, Dictionary<Int, String>.Entry is a Swift type. But in Kotlin, the equivalent type is Dictionary.Entry<Int, String>. When it is used as a scope for other types or even static members, Dictionary’s generics disappear.
Fortunately, Skip is able to bridge enough of the divergence between the languages that you may not run into issues in normal, day-to-day use. Skip fully supports:
- ✓ Using built-in generic data structures like
Array,Dictionary, andSet - ✓ Defining your own generic classes, structs, enums
- ✓ Defining and conforming to protocols with
associatedtypes - ✓ Generic functions
- ✓ Generic constraints such as
where T: Equatable
But there are limits to the incompatibilities that Skip can overcome. The following features are not well supported:
- ~ Static members of generic types are limited. Skip can only support static members that either don’t use the defining type’s generics or that can be converted into a generic function that is defined independently of the defining type’s generics
- ~ Generic specialization by type extensions (e.g.
extension C where T: Equatable) is limited - ✕ Inner types on generic outer types are not supported - see the
Dictionaryexample above - ✕ Kotlin does not allow constructor functions to use generics other than those of the defining type
- ✕ Kotlin does not allow
typealiasesto include generic constraints (e.g.where T: Equatable) - ✕
istesting andas?casts do not consider the generic portions of type signatures, because the generic types don’t exist at runtime
The Skip transpiler generally detects unsupported patterns and provides an appropriate error message. You may, however, run into additional limitations as well. Our general advice is to take advantage of generics for straightforward use cases, but to avoid complex generics definitions and constraints.
Reified Types
Section titled “Reified Types”One way to preserve generic information in Kotlin is to use inline functions with reified types. You can read more about this topic in the Kotlin language documentation ↗. Skip automatically converts any Swift function with the @inline(__always) attribute into a Kotlin inline function with reified generics.
@inline(__always) public func f<T>(param: T) { ...}Transpiles to:
inline fun <reified T> f(param: T) { ...}Concurrency
Section titled “Concurrency”Skip does not support Grand Central Dispatch. Rather, it supports Swift’s modern concurrency with async and await, Task and TaskGroup, and actors.
Note that neither @MainActor nor custom actors are features of Kotlin. Skip supports actors by adding its own calls to jump to and from the actor’s isolated context. You will see these inserted calls in the generated Kotlin, and they may look surprising.
Currently @MainActor is not automatically inherited from superclass and protocol members. Add the attribute to all overrides explicitly. Skip does, however, make an exception for View.body - your View bodies will automatically be @MainActor-bound.
Enums and case matching
Section titled “Enums and case matching”Skip transpiles enums to Kotlin enums, along with creating similar case statements. There are some comlpex case matching constructions that Kotlin doesn’t support, meaning you would need to find and alternative way of expressing the logic in Swift. These limitation assume:
- Cannot translate compound case matches with associated values, like
case .caseA(let value), .caseB(let value): - Cannot translate case matches that also conditionally check values, like
case .caseA(let value) where value == "X"
If Let
Section titled “If Let”Swift’s if let x = f() (or guard let x = f()) syntax does a few things at the same time:
- Executes
f()exactly once. - Tests that the value is not
nil. - Binds the value to a new variable with the appropriate scope.
While Kotlin’s if (x != null) checks do have some intelligence - Kotlin will usually let you treat x as non-null in the body of of the if block - there is no Kotlin language construct that can do all of the things if let does. Depending on the details of how your Swift code uses if let, therefore, Skip may have to generate a significant amount of Kotlin to ensure identical behavior across platforms. This includes generating nested if statements and potentially duplicating entire else code blocks. While the resulting Kotlin may look complicated, it is no less efficient than the original Swift.
Transpilation Examples
Section titled “Transpilation Examples”The following examples show the approximate Kotlin code that Skip’s transpilation will generate for various common Swift code. These may vary depending on project context and Skip version (these were generated with Skip 1.7.0).
Basic Class
Section titled “Basic Class”class MyClass { static let staticValue = 1 var stringField = "abc" var intField = 123 var doubleField = 456.78}internal open class MyClass { internal open var stringField = "abc" internal open var intField = 123 internal open var doubleField = 456.78
companion object { internal val staticValue = 1 }}Class Inheritance
Section titled “Class Inheritance”public class Base { var type: String init(type: String = "Base") { self.type = type }}
internal class Sub : Base { var level: Int init(level: Int) { super.init(type: "Sub") self.level = level }}open class Base { internal open var type: String internal constructor(type: String = "Base") { this.type = type }
companion object: CompanionClass() { } open class CompanionClass { }}
internal open class Sub: Base { internal open var level: Int internal constructor(level: Int): super(type = "Sub") { this.level = level }
companion object: Base.CompanionClass() { }}Immutable Struct
Section titled “Immutable Struct”struct MyStruct { let str: String = "abc" let num: Int}internal class MyStruct { internal val str: String = "abc" internal val num: Int
constructor(num: Int) { this.num = num }}Mutable Struct
Section titled “Mutable Struct”// Skip adds methods to mutable structs that allow Skip to replicate Swift struct value semanticsstruct MyStruct { var str: String = "abc" var num: Int}// Skip adds methods to mutable structs that allow Skip to replicate Swift struct value semanticsinternal class MyStruct: MutableStruct { internal var str: String set(newValue) { willmutate() field = newValue didmutate() } internal var num: Int set(newValue) { willmutate() field = newValue didmutate() }
constructor(str: String = "abc", num: Int) { this.str = str this.num = num }
override var supdate: ((Any) -> Unit)? = null override var smutatingcount = 0 override fun scopy(): MutableStruct = MyStruct(str, num)}Hashable & Codable Struct
Section titled “Hashable & Codable Struct”struct MyStruct : Hashable, Codable { var str: String = "abc" var num: Int}internal class MyStruct: Codable, MutableStruct { internal var str: String set(newValue) { willmutate() field = newValue didmutate() } internal var num: Int set(newValue) { willmutate() field = newValue didmutate() }
constructor(str: String = "abc", num: Int) { this.str = str this.num = num }
override var supdate: ((Any) -> Unit)? = null override var smutatingcount = 0 override fun scopy(): MutableStruct = MyStruct(str, num)
override fun equals(other: Any?): Boolean { if (other !is MyStruct) return false return str == other.str && num == other.num }
override fun hashCode(): Int { var result = 1 result = Hasher.combine(result, str) result = Hasher.combine(result, num) return result }
private enum class CodingKeys(override val rawValue: String, @Suppress("UNUSED_PARAMETER") unusedp: Nothing? = null): CodingKey, RawRepresentable<String> { str("str"), num("num");
companion object { fun init(rawValue: String): CodingKeys? { return when (rawValue) { "str" -> CodingKeys.str "num" -> CodingKeys.num else -> null } } } }
override fun encode(to: Encoder) { val container = to.container(keyedBy = CodingKeys::class) container.encode(str, forKey = CodingKeys.str) container.encode(num, forKey = CodingKeys.num) }
constructor(from: Decoder) { val container = from.container(keyedBy = CodingKeys::class) this.str = container.decode(String::class, forKey = CodingKeys.str) this.num = container.decode(Int::class, forKey = CodingKeys.num) }
companion object: DecodableCompanion<MyStruct> { override fun init(from: Decoder): MyStruct = MyStruct(from = from)
private fun CodingKeys(rawValue: String): CodingKeys? = CodingKeys.init(rawValue = rawValue) }}Basic Protocol
Section titled “Basic Protocol”protocol MyContract { var name: String { get } func perform() throws -> Int}internal interface MyContract { val name: String fun perform(): Int}Protocol with Extension
Section titled “Protocol with Extension”protocol MyContract { var name: String { get } func perform() throws -> Int}
extension MyContract { var name: String { "default" }}internal interface MyContract { val name: String get() = "default" fun perform(): Int}Protocol with associatedtype
Section titled “Protocol with associatedtype”protocol MyContract { associatedtype R var name: String { get } func perform() throws -> R}
class MyClass: MyContract { var name: String var value: Int
init(name: String, value: Int) { self.name = name self.value = value }
func perform() -> Int { return value }}internal interface MyContract<R> { val name: String fun perform(): R}
internal open class MyClass: MyContract<Int> { override var name: String internal open var value: Int
internal constructor(name: String, value: Int) { this.name = name this.value = value }
override fun perform(): Int = value}Basic Enum
Section titled “Basic Enum”enum Size { case small, medium, large}
enum Position : CaseIterable { case one, two, three, four, five}import skip.lib.Array
internal enum class Size { small, medium, large;}
internal enum class Position: CaseIterable { one, two, three, four, five;
companion object: CaseIterableCompanion<Position> { override val allCases: Array<Position> get() = arrayOf(one, two, three, four, five) }}RawRepresentable Enum
Section titled “RawRepresentable Enum”enum Size : String { case small, medium, large = "LG"}internal enum class Size(override val rawValue: String, @Suppress("UNUSED_PARAMETER") unusedp: Nothing? = null): RawRepresentable<String> { small("small"), medium("medium"), large("LG");
companion object { fun init(rawValue: String): Size? { return when (rawValue) { "small" -> Size.small "medium" -> Size.medium "LG" -> Size.large else -> null } } }}
internal fun Size(rawValue: String): Size? = Size.init(rawValue = rawValue)Enum with Associated Values
Section titled “Enum with Associated Values”// Sealed classes are Kotlin's closest analog to enums with associated valuesenum E : Hashable { case option1 case option2(Int, String)}
func process(e: E) { if case .option2(let i, _) = e { print("i = \(i)") }}// Sealed classes are Kotlin's closest analog to enums with associated valuesinternal sealed class E { class Option1Case: E() { } class Option2Case(val associated0: Int, val associated1: String): E() { override fun equals(other: Any?): Boolean { if (other !is Option2Case) return false return associated0 == other.associated0 && associated1 == other.associated1 } override fun hashCode(): Int { var result = 1 result = Hasher.combine(result, associated0) result = Hasher.combine(result, associated1) return result } }
companion object { val option1: E = Option1Case() fun option2(associated0: Int, associated1: String): E = Option2Case(associated0, associated1) }}
internal fun process(e: E) { if (e is E.Option2Case) { val i = e.associated0 print("i = ${i}") }}Dictionary
Section titled “Dictionary”let d = [1: "a", 2: "b", 3: "c"]for (key, value) in d { print(key) print(value)}internal val d = dictionaryOf(Tuple2(1, "a"), Tuple2(2, "b"), Tuple2(3, "c"))for ((key, value) in d.sref()) { print(key) print(value)}Optional Constructor
Section titled “Optional Constructor”// Kotlin does not have optional constructors. Skip replicates their behaviorclass C { let i: Int
init?(param: Int) { if param == 0 { return nil } else { i = param } }}func f() -> Int { return C(param: 0)?.i ?? -1}func g() -> C { return C(param: 0)!}// Kotlin does not have optional constructors. Skip replicates their behaviorinternal open class C { internal val i: Int
internal constructor(param: Int) { if (param == 0) { throw NullReturnException() } else { i = param } }}internal fun f(): Int { return (try { C(param = 0) } catch (_: NullReturnException) { null })?.i ?: -1}internal fun g(): C = C(param = 0)Async Function
Section titled “Async Function”func compute(_ input1: Int, _ input2: Int) async throws -> Int { // Do some expensive things return input1 * input2}
func printResult() async throws { let result = try await compute(3, 5) print(result)}internal suspend fun compute(input1: Int, input2: Int): Int = Async.run l@{ // Do some expensive things return@l input1 * input2}
internal suspend fun printResult(): Unit = Async.run { val result = compute(3, 5) print(result)}Type Inference
Section titled “Type Inference”enum Size { case small, medium, large}
func mySizes() -> [Size] { // Kotlin does not have implicitly-qualified members [.medium, .large]}import skip.lib.Array
internal enum class Size { small, medium, large;}
internal fun mySizes(): Array<Size> { // Kotlin does not have implicitly-qualified members return arrayOf(Size.medium, Size.large)}SwiftUI
Section titled “SwiftUI”import SwiftUI
struct ContentView : View { @State var value = 0.0
var body: some View { VStack { Text("Current value: \(value)") Slider(value: $value) Button("Reset") { value = 0.0 } } }}import androidx.compose.runtime.Composableimport androidx.compose.runtime.getValueimport androidx.compose.runtime.mutableStateOfimport androidx.compose.runtime.rememberimport androidx.compose.runtime.saveable.Saverimport androidx.compose.runtime.saveable.rememberSaveableimport androidx.compose.runtime.setValue
import skip.ui.*import skip.foundation.*import skip.model.*
internal class ContentView: View { internal var value: Double get() = _value.wrappedValue set(newValue) { _value.wrappedValue = newValue } internal var _value: skip.ui.State<Double>
override fun body(): View { return ComposeBuilder { composectx: ComposeContext -> VStack { -> ComposeBuilder { composectx: ComposeContext -> Text({ val str = LocalizedStringKey.StringInterpolation(literalCapacity = 0, interpolationCount = 0) str.appendLiteral("Current value: ") str.appendInterpolation(value) LocalizedStringKey(stringInterpolation = str) }()).Compose(composectx) Slider(value = Binding({ _value.wrappedValue }, { it -> _value.wrappedValue = it })).Compose(composectx) Button(LocalizedStringKey(stringLiteral = "Reset")) { -> value = 0.0 }.Compose(composectx) ComposeResult.ok } }.Compose(composectx) } }
@Composable @Suppress("UNCHECKED_CAST") override fun Evaluate(context: ComposeContext, options: Int): kotlin.collections.List<Renderable> { val rememberedvalue by rememberSaveable(stateSaver = context.stateSaver as Saver<skip.ui.State<Double>, Any>) { mutableStateOf(_value) } _value = rememberedvalue
return super.Evaluate(context, options) }
constructor(value: Double = 0.0) { this._value = skip.ui.State(value) }}Conditional Compilation
Section titled “Conditional Compilation”// Use #if os(Android) to conditionally include code for Android and non-Android platformsfunc languageName() -> String { #if os(Android) "Kotlin" #else "Swift" #endif}
func languageName2() -> String { #if !os(Android) "Swift" #else "Kotlin" #endif}// Use #if os(Android) to conditionally include code for Android and non-Android platformsinternal fun languageName(): String = "Kotlin"
internal fun languageName2(): String = "Kotlin"SKIP Comments
Section titled “SKIP Comments”// Skip has a set of special comments for specifying Kotlin codefunc languageName() -> String { // SKIP REPLACE: return "Kotlin" "Swift"}// Skip has a set of special comments for specifying Kotlin codeinternal fun languageName(): String = "Kotlin"