SKIE Migration Guide

· 16 min read

Author: Kevin Galligan, Sam Hill, Russell Wolf, Jigar Brahmbhatt
SKIE does an amazing job of improving the Swift-side of a Kotlin API, but it does change your types. How do you safely integrate SKIE into existing code? How do you avoid impacting other developers on your team?

The API produced by the Kotlin compiler for Swift is ‘functional’, but far from ideal for a Swift developer. For the Kotlin developer, they need to understand problem areas and work around them. SKIE automatically fixes many of the most problematic parts of this API. In doing so, however, it changes the API. While we try to minimize breaking changes, there will be breaking changes. In this article you will learn:

  • How SKIE works and what it does to your Xcode Framework output.
  • How each SKIE feature may impact the output, and what to look for.
  • Various strategies to migrating your codebase.
Javascript disabled?
Our site requires Javascript for some sections. Please check that Javascript is enabled.

General Overview

SKIE is augmenting types that are exported to Swift. If you have existing code, those type changes can certainly cause issues.

By default, SKIE will run on all of the code that you export to iOS. Assuming your current code works, changing it with SKIE risks breaking the calling side.

In theory, an ideal setting for SKIE would be to only run on code you change. That would prevent it from breaking code you haven’t changed. However, that would be quite complex to try to build. Instead, SKIE has various ways to target specific code to apply SKIE to, or to skip. Also, some features are more likely to break code than others. With this information, crafting a migration strategy will be straightforward.

How SKIE works.

SKIE instruments the API exported to iOS from the Kotlin compiler when building an Xcode Framework. It is important to understand that SKIE affects that and only that. Put in other terms:

SKIE only changes the API that is visible to iOS

If you look at the exported Objective-C header, the declarations in that file are the public iOS API. Anything in there is what SKIE considers its “area of concern”. Anything that is not in that file is ignored by SKIE.

There are some production-level considerations Kotlin Multiplatform teams should be aware of, including API surface reduction. This is not only for readability but for binary size. We’ve created our Touchlab Pro service to help prepare your team for production KMP development. This includes self-study guides, production best practices, and support options from the KMP experts.

SKIE instruments some Kotlin as a compiler plugin, then later in compilation, after the Kotlin compiler has created the Objective-C header and binary, SKIE generates Swift, then compiles that Swift against that Objective-C header, and links the output directly back into the binary.

SKIE also adds some “header magic” in there to help with types.

The end result is the same Xcode Framework package, but with a different API. Wherever possible, the changes were kept source-compatible, but in any significant project, it is unlikely that you will turn on SKIE and have everything compile.

Although less likely, even if the project compiles, SKIE also changes the semantics of certain features. For example, suspend functions now support cancellation. The suspend functions that the Kotlin compiler generates do not. Although unlikely, it’s possible that adding cancellation support to code that previously didn’t need to worry about it will introduce new issues.

In summary, the API will change, so blindly applying SKIE to existing code is risky.

Different features of SKIE will be more or less likely to cause compile issues, and the scope of changes will depend on how you were using the output of the Kotlin compiler without SKIE.

New Empty Project

If you’re starting a new project with no existing code, integration is easy. Just add the SKIE plugin, and use SKIE’s features from the start. Except for a few specific edge cases, starting with SKIE from the beginning allows you to take advantage of the enhanced API immediately.

For more info on using SKIE in new projects, see the Getting Started Guide.

Existing Code

If you have existing code, the situation can be more complex.

The first thing to consider is what features you want to use from SKIE. By default, 4 are enabled on everything, and one is opt-in:

  • Enums - On
  • Sealed Hierarchies - On
  • Default Arguments - Opt-in with annotations
  • Suspend Functions - On
  • Flows - On
“Default Arguments” is not on by default because the implementation could impact build times depending on your project. Sometimes significantly. If “On”, it will add support for every function with multiple arguments, which includes data classes, etc. The vast majority of places are never directly called, so it is a waste. Using the annotation allows you to choose where to use the feature. We are discussing various approaches to mitigate issues in the future, but for now, it is best to apply the annotation in places you would like to make it available

For new projects, we would recommend keeping these defaults.

For existing code, it depends on how you are currently calling some of these structures.

Potential Issues By Feature

Sealed Classes

Sealed classes are easier to address. SKIE generates parallel enums in Swift, and the onEnum function to assist in accessing them. However, SKIE does nothing to the sealed classes themselves. If you are currently accessing a sealed class from Swift, adding SKIE won’t force you to change anything.

Generally speaking, all projects can enable sealed classes without breaking their code.

Summary: You can turn on sealed class support without much concern.

Enums

For Kotlin enums, SKIE generates new Swift enums to match them, “hides” the Kotlin enum from Swift by prepending two underscores to the name, then automatically transforms the Kotlin enum to the Swift enum when going back and forth between Kotlin and Swift.

The automatic transform happens by way of an interop feature that Apple added for Objective-C to Swift interop.

See the SKIE Demo post and project to better understand how this works.

In the majority of cases, that means you can turn on enum support for existing code and everything should work. However, Kotlin enums and Swift enums are different. There are certain cases where your code may break, and you’ll need to be aware of mitigations, or suppress enums in certain spots. Details about potential enum issues are covered later in this guide.

Summary: Enums should be drop-in compatible, but can cause compilation issues depending on where and how you use them.

Default Arguments

Default arguments exist in both Kotlin and Swift, but they are semantically quite different. As a result, the SKIE’s current implementation is to generate combinations of functions in Kotlin rather than generating Swift. If you have a function and add default arguments support in SKIE, it shouldn’t break, so they are safe to turn on from a compilation perspective. However, the default arguments feature is opt-in by default (by “opt-in” we mean you need to put an annotation on the function).

Summary: Default arguments will not cause compilation issues, and they’re off by default, so this isn’t much of a migration issue, unless you turn them on globally.

Suspend Functions

The Kotlin compiler will create a function signature for Objective-C that exposes a callback. This convention let’s Swift call the function with async syntax, but without cancellation. If you are using Swift’s async/await syntax to call Kotlin suspend functions, SKIE’s versions should work with minimal or no changes.

However, if you are using the callback syntax of the Kotlin compiler’s suspend output, that will likely break because the functions are different.

Also, it is possible that the semantic differences between the default suspend functions and SKIE’s suspend functions could be a problem. For example, if the Swift code called a suspend function for some operation then exited, which triggered a shutdown of the async context, it could cancel the suspend function call. Since the default suspend functions out of the Kotlin compiler do not support cancellation, that suspend function call would continue. SKIE adds cancellation to suspend functions, which is better overall, but in the theoretical case here, would lead to cancelled operations.

Summary: Suspend function support from SKIE can cause migration issues, but it depends on how you are currently using them.

Flow

The Kotlin compiler does export Flow, but attempting to use Flow directly as exported is very difficult. Generally speaking, if you have a Flow exported to iOS, you probably aren’t calling it directly. In that case, because you’re probably not calling them, running SKIE on Flow instances is unlikely to impact existing code.

However, you can use Flow directly (and we’ve seen code that does this). If that’s the case, SKIE will certainly break your existing code, so be aware.

A much more likely, problematic scenario, is if you’re using another Coroutines interop library. This might include something like CFlow, or more recently, KMP-NativeCoroutines. Turing on SKIE’s Flow support and using any of these other solutions together is likely to break. If you are using some other Flow interop solution and intend to continue, it would make sense to simply disable SKIE’s Flow support. If you are going to use both, you’ll want to make sure each instance of Flow is exported with only one interop handler.

Summary: It depends how you’re currently using Flow, and if you have another Flow interop library in place.

Migration Strategy

There are two broad strategies for “migrating” code when using SKIE. Incremental and “all at once”. Incremental changes can come in different forms, but this does allow a less severe update. On the other hand, incremental updates creates a situation where different code works in different ways, and may introduce more confusion. If possible, simply changing everything is generally preferable.

All At Once

The simplest approach is to apply SKIE to everything and change the consuming Swift code to call the changed API. If you can do this, it is a better option. Having different tools working on different parts of the code can be confusing.

“All At Once” can be somewhat incremental, but by feature rather than package or using annotations. For example, if you use the callbacks for suspend functions, SKIE will break those calls. Applying SKIE’s other features, but delaying suspend functions, may be a reasonable approach.

If you have a large code base, if your team is risk averse, if you are publishing an SDK and can’t dictate the consumer’s timelines. These are all situations where a hard migration probably won’t work well.

Get Migration Help
Touchlab engineers can work directly with your team on migration.

Incremental

Incremental migration is, basically, migrating over time. There are various approaches to this, and some combination of them will probably be the best option.

By Feature

As discussed above, the various features of SKIE are more or less likely to cause issues when applied to existing code. The first step should be to review and understand how your code is using these features currently, and enable those features that are unlikely to be an issue.

For any existing codebase, a reasonable approach would be to enable sealed classes and enums, then see if that causes any problems.

Because sealed class support from SKIE doesn’t actually change the sealed classes themselves, it is very unlikely to be a problem, and if it was, that would be a bug either in SKIE or the Swift compiler (we found several of these).

Enums should generally work, but there are cases where they will break code. See below. Generally speaking, though, when they do, the Swift code change needed to fix the breaks is minimal. Alternatively, you can use annotations to disable specific enum transformations. Coroutines interop, suspend functions and Flow support, are more likely to be a problem, but only if you are currently calling them from Swift, or are using another Coroutines interop library. If not, then these should also be enabled. If you are currently calling them, this is the area that will most likely need time to update.

By Changes

In a perfect world, we’d have endless time to refactor our code. Most teams do not have the time to do that. We’ve found that in practice, many migration efforts happen by only updating code when you need to change it for feature development.

Basically, migrate code when you’re changing it. That can be updates to existing code or new code being added.

This general approach has it’s pros and cons. If you are changing incrementally, that means you have multiples ways of “doing things”, which can get confusing. One big “update” is more severe, but gets everything done with one shot. A more gradual approach requires less dedicated time up front, and introduces less overall risk. The appropriate approach really depends on your project and your team.

The first step is the same as above. Pick which features, if any, to enable globally. Generally speaking, sealed classes and enums are safe to turn on. Coroutines interop will likely require fixes, and may be best disabled.

As code is changed over time, disabled features can be enabled precisely with annotations. Say you have a class that exposes a Flow and need to change some logic. While you’re there, add the @FlowInterop.Enabled annotation, which will let SKIE run on that Flow.

By Feature/Package

Another approach might be incrementally by “zone” of a code base. SKIE’s Gradle config allows you to specify configuration specific to a package, or to a specific class. If you have features broken out roughly by package, you could schedule migration efforts incrementally that way.

Potential Issues

Sealed Classes

Sealed class support in SKIE doesn’t change the underlying classes themselves, so enabling sealed class support does not directly cause problems. However, SKIE does generate Swift code for them, and depending on how you are integrating this code, it may introduce extra binary size that you aren’t using.

When you are changing Swift code to use SKIE with sealed classes, complex hierarchies of sealed classes and interfaces can be trouble. They’re compile, but the switch semantics can differ between Kotlin and Swift. SKIE had to make compromises in the design of the output for this feature, and we’d like to hear from teams who use sealed hierarchies in unique ways.

Enums

Kotlin enums are replaced bo actual Swift enums, and the change happens automatically. However, Kotlin enums are classes and have functions that aren’t available in Swift enums. Swift enums are not classes. These two differences are the bulk of the potential issues presented by enum support.

The SKIE Enums Doc has a more extensive list of potential issues. The most likely issues, in summary:

Functions

.values() is not available to Swift. However, the Swift enums implement CaseIterable, so Swift code can access the list of values if needed.

valueOf(String) is also not available, but you can create instances of Swift enums with the following:

let foo = Foo(rawValue: "bar")!

Generics

Kotlin generics all need to be classes. Swift enums are not classes. If you have an enum in a generic in Kotlin, from Swift, that will be the actual Kotlin type rather than the Swift enum.

For the following Kotlin code:

enum class Foo {
    Bar, Baz
}

data class ResultWrapper<T:Any>(val t:T)

fun enumResult():ResultWrapper<Foo>{
    return ResultWrapper(Foo.Bar)
}

When calling enumResult() from Swift, the return type will be ResultWrapper<__Foo>. When SKIE generates Swift enums, it also renames the underlying Kotlin enums with __ as a prefix. That prefix “hides” them from Xcode autocomplete, but they are still available. You can convert back and forth with the following Swift code:

let result = SomeFileKt.enumResult()
let enumVal:__Foo = result.t

let swiftEnum:Foo = enumVal.toSwiftEnum()
let kotlinEnum:__Foo = swiftEnum.toKotlinEnum()

Interfaces

Kotlin enums can implement interfaces, but they won’t be supported on the Swift side. If you need them from Swift, it would be best to disable SKIE enum support for that enum.

Default Arguments

This feature would only cause direct issues if there’s some kind of SKIE bug that we aren’t yet aware of. However, the feature has some constraints and potential impacts that are important to understand.

We don’t globally enable default arguments by default. There are a lot of places in Kotlin code that may define default arguments that you aren’t aware of, and most likely will never call from Swift. A great example would be the copy function for data classes. If you have a lot of data classes, SKIE will create a matrix of functions to implement copy, and in most cases, you’ll never call copy from Swift, so it wasted effort.

Because of how default arguments are currently implemented, applying the feature for functions you aren’t going to call from Swift is a waste. In some testers codebases, globally enabling default arguments significantly increased compilation time, and certainly added extra binary that gave no benefit to the project.

In summary, don’t enable default arguments unless you know what you’re doing.

Another complication for default arguments is the max parameter count. Again, default arguments are implemented by generating the set of functions necessary to present to Swift as if they were true “default arguments” as Swift expects them. That means the number of functions generated is not linear. SKIE has a cap on the number of parameters it will generate these functions for, and if there are more than that cap, that function will be skipped. See SKIE Default Arguments doc for more information.

Suspend Functions

Generally speaking, any issues with suspend functions should be pretty obvious when compiling. If your code expects the default Kotlin compile output, the Swift bulid will fail after SKIE is applied. If your Swift code uses the async/await syntax to call suspend functions, it’ll probably compile fine.

The obvious potential issue is if your code is not expecting cancellation support. If you initiate a call to a suspend function from Swift with the default Kotlin compiler output, it will continue, even if the caller context goes away. With SKIE, that changes, and it won’t be immediately obvious that that is an issue.

For example, imagine a note taking app with an edit screen. When you click “save”, the app starts a server call and a db save, and the screen closes so the use goes back to the note list page. If closing the screen shuts down the async context in which the suspend call was started, with SKIE enabled, that call may be cancelled silently. Depending on how that interaction was coded, it may only happen intermittently.

Summary: consider what automatic cancellation support might do to code that does not expect it.

Flow Support

Potential issues with Flow support are less subtle. If you are using some other Coroutines interop library, it is highly recommended that you do not try to use that and SKIE on the same individual Flow instance (it’s fine to use both in the same project). However, if you do try to use both, you’ll almost certainly get a compilation error right away, so at least you’ll be aware of the problem :)

One real difference between other Coroutines interop solutions and SKIE is that SKIE does not throw exceptions from the underlying Flow. Your Kotlin code will need to deal with exceptions appropriately in its logic rather than expecting that to throw to Swift. Philosophically, this is how we (Touchlab) generally view error handling and KMP. If you’re expecting an error, catch it and make error states an explicit part of the API rather than just letting them bubble up to Swift. However, other developers may not prefer that approach. Please reach out with feedback as we are thinking through error propagation options for Flow support.

To be precise, “SKIE does not throw exceptions from the underlying Flow” is not quite correct. CancellationException is a special case, and if that exception is thrown, the AsyncSequence will get proper notification of cancellation.

Coming Soon…

We have feedback from actual testers who have been working with SKIE for several months. We are currently working through that feedback to present a summary here. Please check back over the coming days for updates.