Swift Support Reference
Skipβs Swift to Kotlin language transpiler is able to convert a large subset of the Swift language into Kotlin. 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
The following table details Skipβs support for 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
- β
Codable
synthesis
- β Structs
- β Value semantics. See the Structs topic below
- β Constructor synthesis
- β
Equatable
synthesis - β
Hashable
synthesis - β
Codable
synthesis
- β Protocols
- β Enums
- β Enums with associated values
- β
RawRepresentable
synthesis - β
CaseIterable
synthesis - β
Equatable
synthesis - β
Hashable
synthesis - ~
Codable
synthesis- Skip can only synthesize
Codable
conformance forRawRepresentable
enums
- 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
- β
self
assignment in mutable functions - β Default parameter values
- β
inout
parameters - β Closures and trailing closures
- β Variadic parameters
- β
@autoclosure
parameters - β Parameter packs
- β Nested functions
- β Constructors
- β Optional constructors
- β
self
assignment in constructors - ~ Kotlin imposes some limitations on calling
super.init
orself.init
in a delegating constructor - β Constructors cannot use generic parameter types that are not declared by the owning type
- β Deconstructors
- ~
deinit
is 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
Equatable
with==
- β Custom
Hashable
withhash(into:)
- β Custom
Comparable
with<
- ~ Custom subscript operators
- Cannot overload subscript operators on parameter labels or types
- β
callAsFunction
support - β Other custom operators
- ~ Key paths
- β As implicit closure parameters
- β As
@Environment
keys - β Other uses
- β Macros
- β
@Observable
- β
@ObservationIgnored
- β Other macros
- β
Builtin Types
The following table details Skipβs support for builtin Swift standard library types. Support for these types is divided between the Skip language transpiler and the SkipLib open source library.
Not all API is available on all types. Consult the SkipLib library for current status.
- β Numeric types
- β Use Kotlin native types
- ~
Int
is 32 bit on JVM - ~ All unsigned and
Float
values 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
Optional
as its own type, so.some
and.none
do 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
OptionSet
with 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
@ViewBuilder
attribute is not inherited when overriding API other thanView.body
. Specify it explicitly
- The
- β Other result builders
- β
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
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
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 append
ing 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
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
deinit
functions will be called at an indeterminate time, and may not be called at all. While Swift callsdeinit
functions 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
weak
andunowned
modifiers 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 ignoreweak
andunowned
modifiers on properties and in closure capture lists, relying on the garbage collector instead. If you were planning to use aweak
orunowned
reference for reasons other than avoiding a strong reference cycle, you should consider alternatives.
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
MutableStruct
protocol 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 ATTRIBUTES: nocopy
struct S {
β¦
}
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
Dictionary
example above - β Kotlin does not allow constructor functions to use generics other than those of the defining type
- β Kotlin does not allow
typealiases
to include generic constraints (e.g.where T: Equatable
) - β
is
testing 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.
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.
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.