7.3.2 使用 Swift 导出与 Swift 互操作

原文链接: https://kotlinlang.org/docs/native-swift-export.html

7.3.2 使用 Swift 导出与 Swift 互操作

Alpha

Kotlin 通过 Swift 导出实现的与 Swift 的互操作目前处于 Alpha 阶段。Swift 导出让你可以直接导出 Kotlin 源码,并以符合 Swift 习惯的方式从 Swift 调用 Kotlin 代码,无需 Objective-C 头文件。

Swift 导出让面向 Apple 目标的多平台开发更加顺畅。例如,如果你有一个包含顶层函数的 Kotlin 模块,Swift 导出可以基于模块进行简洁的导入,从而消除令人困惑的 Objective-C 下划线和被改写的名称。

目前的 Swift 导出特性包括:

  • 多模块支持。每个 Kotlin 模块都作为单独的 Swift 模块导出,简化函数调用。
  • 包支持。导出时会显式保留 Kotlin 包,避免生成的 Swift 代码中出现命名冲突。
  • 类型别名。Kotlin 类型别名会被导出并保留到 Swift 中,提升可读性。
  • 原始类型可空性的增强。不同于 Objective-C 互操作(它需要把 Int? 等类型装箱为 KotlinInt 之类的包装类来保留可空性),Swift 导出会直接转换可空性信息。
  • 重载。你可以在 Swift 中无歧义地调用 Kotlin 的重载函数。
  • 扁平化包结构。你可以把 Kotlin 包转换为 Swift 枚举,从而从生成的 Swift 代码中移除包前缀。
  • 模块名自定义。你可以在 Kotlin 项目的 Gradle 配置中自定义生成的 Swift 模块名。
  • 并发支持。你可以从 Swift 无缝调用挂起的 Kotlin 代码,并开箱即用地把 kotlinx.coroutines 的 flow 导出为 Swift 的 AsyncSequence。

启用 Swift 导出

Swift 导出目前处于 Alpha 阶段,仍不完整,因此预计会有破坏性变更。要试用它,请在你的 Kotlin 项目中配置构建文件,并设置 Xcode 以集成 Swift 导出。

配置 Kotlin 项目

你可以把以下构建文件作为项目中设置 Swift 导出的起点:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// build.gradle.kts
kotlin {

    iosArm64()
    iosSimulatorArm64()

    swiftExport {
        // 设置根模块名
        moduleName = "Shared"

        // 设置折叠规则
        // 从生成的 Swift 代码中移除包前缀
        flattenPackage = "com.example.sandbox"

        // 配置外部模块的导出
        export(project(":subproject")) {
            // 设置导出模块的名称
            moduleName = "Subproject"
            // 为导出的依赖设置折叠规则
            flattenPackage = "com.subproject.library"
        }

        // 为链接任务提供编译器参数
        configure {
            freeCompilerArgs.add("-Xexpect-actual-classes")
        }
    }
}

Kotlin 编译器会自动生成所有必要的文件(包括 swiftmodule 文件、静态 .a 库、头文件和 modulemap 文件),并把它们复制到应用的构建目录中,你可以从 Xcode 访问该目录。

提示: 你也可以克隆我们已配置好 Swift 导出的公开示例。

配置 Xcode 项目

要配置 Xcode 以把 Swift 导出集成到你的项目中:

  1. 在 Xcode 中打开项目设置。
  2. 在 Build Phases 选项卡中,找到包含 embedAndSignAppleFrameworkForXcode 任务的 Run Script 阶段。
  3. 在运行脚本阶段把该脚本替换为 embedSwiftExportForXcode 任务:
1
   ./gradlew :<Shared module name>:embedSwiftExportForXcode

添加 Swift 导出脚本

  1. 构建项目。构建会在输出目录中生成 Swift 模块。

目前的限制

Swift 导出目前仅在通过直接集成把 iOS framework 连接到 Xcode 项目的项目中可用。这是使用 IntelliJ IDEA 中的 Kotlin Multiplatform 插件或通过网页向导创建的 Kotlin Multiplatform 项目的标准配置。

其他已知问题:

  • 继承自 List、Set 或 Map 的类型在导出时会被忽略(KT-80416)。
  • List、Set 或 Map 的继承者无法在 Swift 一侧实例化(KT-80417)。
  • 导出到 Swift 时,Kotlin 泛型类型参数会被类型擦除为其上界。
  • 没有可用的 IDE 迁移提示或自动化工具。
  • 当使用需要选择启用的声明时,你必须在 Gradle 构建文件的_模块级别_添加显式的 optIn 编译器选项。例如对于 kotlinx.datetime 库:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
  swiftExport {
      moduleName = "Shared"

      export("org.jetbrains.kotlinx:kotlinx-datetime:0.8.0") {
          moduleName = "KotlinDateTime"
          flattenPackage = "kotlinx.datetime"
      }
  }

  // 在模块级别添加单独的选择启用块
  compilerOptions {
      optIn.add("kotlin.time.ExperimentalTime")
  }

映射

下表展示了 Kotlin 概念如何映射到 Swift。

| Kotlin | Swift |

| class | class | | object | 带 shared 属性的 class | | enum class | enum | | sealed 类和接口 | enum | | typealias | typealias | | 函数 | 函数 | | suspend fun | async | | kotlinx.coroutines flow | AsyncSequence | | 属性 | 属性 | | 构造器 | 初始化器 | | 包 | 嵌套枚举 | | Boolean | Bool | | Char | Unicode.UTF16.CodeUnit | | Byte | Int8 | | Short | Int16 | | Int | Int32 | | Long | Int64 | | UByte | UInt8 | | UShort | UInt16 | | UInt | UInt32 | | ULong | UInt64 | | Float | Float | | Double | Double | | Any | KotlinBase 类 | | Unit | Void | | Nothing | Never |

声明

类

Swift 导出只支持直接继承自 Any 的 final 类,例如 class Foo()。它们会被转换为继承自特殊 KotlinBase 类的 Swift 类:

1
2
3
4
5
6
// Kotlin
class MyClass {
    val property: Int = 0

    fun method() {}
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Swift
public class MyClass : KotlinRuntime.KotlinBase {
    public var property: Swift.Int32 {
        get {
            // ...
        }
    }
    public override init() {
        // ...
    }
    public func method() -> Swift.Void {
        // ...
    }
}

对象

对象会被转换为带私有 init 和静态 shared 访问器的 Swift 类:

1
2
// Kotlin
object O
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// Swift
public class O : KotlinRuntime.KotlinBase {
    public static var shared: O {
        get {
            // ...
        }
    }
    private override init() {
        // ...
    }
}

类型别名

Kotlin 类型别名会按原样导出:

1
2
// Kotlin
typealias MyInt = Int
1
2
// Swift
public typealias MyInt = Swift.Int32

枚举

Kotlin enum class 声明会作为常规的原生 Swift enum 类型导出:

1
2
3
4
5
6
7
8
// Kotlin
enum class Color(val rgb: Int) {
    RED(0xFF0000),
    GREEN(0x00FF00),
    BLUE(0x0000FF)
}

val color = Color.RED
1
2
3
4
5
6
// Swift
public enum Color: Swift.CaseIterable, Swift.LosslessStringConvertible, Swift.RawRepresentable {
    case RED, GREEN, BLUE

    public var rgb: Swift.Int32 { get }
}

密封类与接口

Kotlin 中定义的密封层次结构会映射为 Swift 枚举,从而支持穷尽的 switch 语句。

Swift 导出会为每个密封类型生成一个 .sealedType() 方法。该方法返回一个 Swift 枚举,其 case 与密封层次结构的直接子类相对应。你可以嵌套这些调用以匹配更深层次的层次结构。

例如,在 Kotlin 中声明一个带类层次结构的密封接口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// Kotlin
sealed interface Shape

class Circle : Shape {
    override fun toString(): String = "Circle"
}

class Rectangle : Shape {
    override fun toString(): String = "Rectangle"
}

fun createCircle(): Shape = Circle()

在 Swift 一侧,你可以使用不带 default 分支的穷尽 switch:

1
2
3
4
5
6
7
8
// Swift
let shape = createCircle()

let name = switch shape.sealedType() {
    case let .circle(type): "It's a \(type.value)"
    case let .rectangle(type): "It's a \(type.value)"
}
// name == "It's a Circle"

由于该 switch 是穷尽的,如果密封层次结构中新增了子类,编译器会给出警告,因此你可以立即处理它,而无需依赖 switch 的 default 分支。

函数

Swift 导出支持简单的顶层函数和方法:

1
2
3
4
// Kotlin
fun foo(a: Short, b: Bar) {}

fun baz(): Long = 0
1
2
3
4
5
6
7
8
// Swift
public func foo(a: Swift.Int16, b: Bar) -> Swift.Void {
    // ...
}

public func baz() -> Swift.Int64 {
    // ...
}

对于 Kotlin 的扩展函数,接收者参数会变成位于第一位的普通 Swift 参数:

1
2
// Kotlin
fun Int.foo(): Unit = TODO()
1
2
// Swift
func foo(_ receiver: Int32) {}

Kotlin 中带 vararg 的函数会映射为 Swift 的可变参数函数参数:

1
2
// Kotlin
fun log(vararg messages: String)
1
2
// Swift
public func log(messages: Swift.String...)

注意:* 目前对带 operator 修饰符的函数的支持有限。* 泛型类型一般不受支持。

属性

Kotlin 属性会被转换为 Swift 属性:

1
2
3
4
5
6
// Kotlin
val a: Int = 0

var b: Short = 15

const val c: Int = 0
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Swift
public var a: Swift.Int32 {
    get {
        // ...
    }
}
public var b: Swift.Int16 {
    get {
        // ...
    }
    set {
        // ...
    }
}
public var c: Swift.Int32 {
    get {
        // ...
    }
}

构造器

构造器会被转换为 Swift 初始化器:

1
2
// Kotlin
class Foo(val prop: Int)
1
2
3
4
5
6
7
8
// Swift
public class Foo : KotlinRuntime.KotlinBase {
    public init(
        prop: Swift.Int32
    ) {
        // ...
    }
}

类型

kotlin.Nothing

Kotlin 的 Nothing 类型会被转换为 Never 类型:

1
2
3
4
// Kotlin
fun foo(): Nothing = TODO()

fun baz(input: Nothing) {}
1
2
3
4
5
6
7
8
// Swift
public func foo() -> Swift.Never {
    // ...
}

public func baz(input: Swift.Never) -> Void {
    // ...
}

分类器类型

Swift 导出目前只支持直接继承自 Any 的 final 类。

包

Kotlin 包会被转换为嵌套的 Swift 枚举,以避免名称冲突:

1
2
3
// Kotlin
// foo.bar 包中的 bar.kt 文件
fun callMeMaybe() {}
1
2
3
// Kotlin
// foo.baz 包中的 baz.kt 文件
fun callMeMaybe() {}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// Swift
public extension foo.bar {
    public func callMeMaybe() {}
}

public extension foo.baz {
    public func callMeMaybe() {}
}

public enum foo {
    public enum bar {}

    public enum baz {}
}

并发

挂起函数

你可以从 Swift 调用挂起的 Kotlin 代码。Kotlin 的挂起函数和挂起函数类型会导出为 Swift 的 async 对应形式:

1
2
3
4
5
// Kotlin
suspend fun hello(): String {
    delay(1000)
    return "Hello Swift! This is Kotlin."
}
1
2
// Swift
let msg = try await hello()

Flow

你还可以把 kotlinx.coroutines 的 flow 导出为 Swift 的 AsyncSequence:

1
2
3
// Kotlin
// 导出 Flow 时保留 String 类型
fun flowOfStrings(): Flow<String> = flowOf("hello", "any", "world")
1
2
3
4
5
6
7
// Swift
var actual: [String] = []

// 从 Kotlin 推断出 String 类型
for try await element in flowOfStrings().asAsyncSequence() {
    actual.append(element)
}

协程调度器

默认情况下,当你从 Swift 调用 Kotlin 挂起函数或使用 asAsyncSequence 函数时,Kotlin 会创建一个使用 Dispatchers.Default 调度器的协程上下文,并在其中执行导出的代码。

要在其他调度器上运行导出的代码,请在 Kotlin 中使用 withContext() 函数切换协程上下文。例如:

1
2
3
4
suspend fun runOnMain(): Int = withContext(Dispatchers.Main) {
    delay(10L)
    42
}

跨语言继承

Swift 导出支持跨语言继承。该特性的一个常见用例是反向导入模式:你在 Kotlin 中定义契约,并在 Swift 一侧提供平台特有的实现。当你需要使用无法直接导入 Kotlin 的纯 Swift 库时,这特别有用。

要实现这种模式,你需要声明一个 Kotlin 接口和一个 Swift 实现可以继承的 Kotlin 父类。然后你在 Swift 中实现该接口,并把该 Swift 对象传给接受该接口的 Kotlin 函数。例如,对于 CryptoKit 库:

  1. 在 Kotlin 一侧,声明一个接口、一个接受该接口的函数,以及一个 open 基类:
1
2
3
4
5
6
7
8
    // Kotlin
    interface CryptoProvider {
        fun hashMD5(input: String): String
    }

    fun processHash(provider: CryptoProvider, input: String): String = provider.hashMD5(input)

    open class SwiftBase
  1. 在 Swift 一侧,继承导出的 SwiftBase 类,用纯 Swift 库实现该接口,并把该对象传回 Kotlin:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
    // Swift
    import CryptoKit

    final class IosCryptoProvider: SwiftBase, CryptoProvider {
        func hashMD5(input: String) -> String {
            guard let data = input.data(using: .utf8) else { return "failed" }
            return Insecure.MD5.hash(data: data).description
        }
    }

    let provider = IosCryptoProvider()

    // 调用 Kotlin 函数,后者会回调 Swift 中的 hashMD5()
    print(processHash(provider: provider, input: "Hello, world!"))

当 Kotlin 接收到该 Swift 对象时,会把它当作普通 Kotlin 接口的实现来对待,直接调用 Swift 代码。

Swift 导出的演进

我们计划在未来的 Kotlin 版本中扩展并逐步稳定 Swift 导出,改善 Kotlin 与 Swift 之间的互操作。你可以留下你的反馈: