Menu

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:

  1. 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.
  2. 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.
  3. 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
    • ✓ Inheritance
    • ✓ Property requirements
    • ✓ Function requirements
    • ✕ Constructor requirements
    • ✕ Static requirements
  • ✓ Enums
    • ✓ Enums with associated values
    • RawRepresentable synthesis
    • CaseIterable synthesis
    • Equatable synthesis
    • Hashable synthesis
    • ~ Codable synthesis
      • Skip can only synthesize Codable conformance for RawRepresentable enums
  • ✓ Nested 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
    • ✓ Generic classes, structs, enums
    • ✓ Protocol associatedtypes
    • ✓ Common cases covered
    • ~ Limited support for static generic members
      • Kotlin statics do not have access to their owning type’s generics
    • ✕ Self constraints
    • is testing and as? casts do not consider the generic portions of type signatures
      • Kotlin generics are erased at runtime
  • ✓ 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
  • ✓ 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 or self.init in a delegating constructor
    • ✕ Constructors cannot use generic parameter types that are not declared by the owning type
  • ✓ Deconstructors
  • ✓ Closures
    • ✓ Explicit and implicit ($0, $1, etc) parameters
    • ~ Weak and unowned capture is ignored. We rely on Kotlin garbage collection
  • ✓ 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
    • async / await
    • ~ async let
      • The implicit task group is not cancelled when exiting scope
    • ✓ Async functions
    • ✓ Async properties
    • ✓ Async closures
    • @MainActor
    • ✕ Custom actors
    • ✕ Structured concurrency
    • ✕ 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 … when
  • ✓ While loop
  • ✓ Do while loop
  • ✓ For in loop
    • for … in … when …
    • 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 with hash(into:)
    • ✓ Custom Comparable with <
    • ~ Custom subscript operators
      • Cannot overload subscript operators on parameter labels or types
    • ✕ 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 or Double
  • String
    • ✓ Uses Kotlin native String
    • ✕ Mutation is not supported
  • Any, AnyObject
  • ✓ Optionals
    • ✕ Kotlin does not represent Optional as its own type, so .some and .none do not exist
  • ✕ Compound types (e.g. A & B)
  • Array
    • ✓ Value semantics
    • ✓ Slicing
  • Dictionary
    • ✓ Value semantics
  • Set
    • ✓ Value semantics
  • OptionSet
    • ~ You must implement OptionSet with a struct
  • 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 than View.body. Specify it explicitly
    • ✕ 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. You can generally avoid errors by following two simple rules:

  1. Be explicit when using any types other than Int and Double, or when assigning a value of one type to a variable or parameter of another. For example, if var f is a Float, write f = Float(1.0) rather than f = 1.0. Similarly, if a function has a Float parameter, pass the value as Float(1.0).
  2. Although Int and Double do not need explicit casts, do not attempt to assign an integer literal to a double variable or parameter. For example, if var d is a Double, write d = 1.0 rather than d = 1. When passing a literal value to a parameter that takes a Double, again write it 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 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

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 calls deinit 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 and unowned 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 ignore weak and unowned modifiers on properties and in closure capture lists, relying on the garbage collector instead. If you were planning to use a weak or unowned 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 adds sref() 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 {
    
}

Concurrency

Skip does not support Grand Central Dispatch. Rather, it supports Swift’s modern concurrency with Task, Task.detached, async / await, and async let. These are all forms of unstructured concurrency, however. Skip does not yet support Swift’s modern structured concurrency: actors and task groups.

The only exception to this split is that Skip does support @MainActor. @MainActor is not a feature of Kotlin, so Skip supports it by adding its own calls to jump to and from the main thread. 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:

  1. Executes f() exactly once.
  2. Tests that the value is not nil.
  3. 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.