跳到主要内容

与 Swift/Objective-C 的互操作性

备注

Objective-C 库的导入是 实验性的。 所有由 cinterop 工具从 Objective-C 库生成的 Kotlin 声明都应该带有 @ExperimentalForeignApi 注解。

Kotlin/Native 附带的 Native 平台库(如 Foundation、UIKit 和 POSIX)仅对某些 API 需要选择加入。

本文档涵盖了 Kotlin/Native 与 Swift/Objective-C 互操作性的一些方面:如何在 Swift/Objective-C 代码中使用 Kotlin 声明,以及如何在 Kotlin 代码中使用 Objective-C 声明。

您可能会发现一些其他有用的资源:

将 Swift/Objective-C 库导入 Kotlin

如果正确导入到构建中,Objective-C 框架和库可以在 Kotlin 代码中使用(默认情况下会导入系统框架)。 有关更多详细信息,请参见:

如果 Swift 库的 API 使用 @objc 导出到 Objective-C,则可以在 Kotlin 代码中使用 Swift 库。 目前尚不支持纯 Swift 模块。

在 Swift/Objective-C 中使用 Kotlin

如果将 Kotlin 模块编译为框架,则可以在 Swift/Objective-C 代码中使用:

从 Objective-C 和 Swift 中隐藏 Kotlin 声明

@HiddenFromObjC 注解是 实验性的 并且需要 选择加入

为了使您的 Kotlin 代码对 Objective-C/Swift 更加友好,您可以使用 @HiddenFromObjC 从 Objective-C 和 Swift 中隐藏 Kotlin 声明。 该注解禁用将函数或属性导出到 Objective-C。

或者,您可以使用 internal 修饰符标记 Kotlin 声明,以限制其在编译模块中的可见性。 如果您只想从 Objective-C 和 Swift 中隐藏 Kotlin 声明,但仍然希望它对其他 Kotlin 模块可见,请选择 @HiddenFromObjC

请参阅 Kotlin-Swift interopedia 中的示例

在 Swift 中使用 refining

@ShouldRefineInSwift 注解是 实验性的 并且需要 选择加入

@ShouldRefineInSwift 有助于用 Swift 编写的包装器替换 Kotlin 声明。 该注解将函数或属性标记为生成的 Objective-C API 中的 swift_private。 此类声明带有 __ 前缀,这使得它们在 Swift 中不可见。

您仍然可以在 Swift 代码中使用这些声明来创建 Swift 友好的 API,但它们不会在 Xcode 自动完成中建议。

更改声明名称

@ObjCName 注解是 实验性的 并且需要 选择加入

要避免重命名 Kotlin 声明,请使用 @ObjCName 注解。 它指示 Kotlin 编译器对带注解的类、接口或其他 Kotlin 实体使用自定义的 Objective-C 和 Swift 名称:

@ObjCName(swiftName = "MySwiftArray")
class MyKotlinArray {
@ObjCName("index")
fun indexOf(@ObjCName("of") element: String): Int = TODO()
}

// 使用 ObjCName 注解的用法
let array = MySwiftArray()
let index = array.index(of: "element")

请参阅 Kotlin-Swift interopedia 中的另一个示例

使用 KDoc 注释提供文档

文档对于理解任何 API 至关重要。 为共享的 Kotlin API 提供文档使您能够就用法、注意事项等问题与其用户进行交流。

默认情况下,生成 Objective-C 标头时,KDocs 注释不会转换为相应的注释。 例如,以下带有 KDoc 的 Kotlin 代码:

/**
* 打印参数的总和。
* 正确处理总和不适合 32 位整数的情况。
*/
fun printSum(a: Int, b: Int) = println(a.toLong() + b)

将生成一个没有任何注释的 Objective-C 声明:

+ (void)printSumA:(int32_t)a b:(int32_t)b __attribute__((swift_name("printSum(a:b:)")));

要启用 KDoc 注释的导出,请将以下编译器选项添加到您的 build.gradle(.kts)

kotlin {
targets.withType<org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget> {
compilations.get("main").compilerOptions.options.freeCompilerArgs.add("-Xexport-kdoc")
}
}

之后,Objective-C 标头将包含相应的注释:

/**
* 打印参数的总和。
* 正确处理总和不适合 32 位整数的情况。
*/
+ (void)printSumA:(int32_t)a b:(int32_t)b __attribute__((swift_name("printSum(a:b:)")));

您将能够在自动完成中看到类和方法的注释,例如,在 Xcode 中。 如果您转到函数的定义(在 .h 文件中),您将看到关于 @param@return 等的注释。

已知限制:

将 KDoc 注释导出到生成的 Objective-C 标头的功能是 实验性的。 它可能随时被删除或更改。 需要选择加入(请参阅下面的详细信息),您应该仅将其用于评估目的。 我们感谢您在 YouTrack 中提供有关它的反馈。

  • 除非依赖项本身使用 -Xexport-kdoc 编译,否则不会导出依赖项文档。 该功能是实验性的,因此使用此选项编译的库可能与其他编译器版本不兼容。
  • KDoc 注释主要按原样导出。许多 KDoc 功能,例如 @property,不受支持。

映射

下表显示了 Kotlin 概念如何映射到 Swift/Objective-C,反之亦然。

"->" 和 "<-" 表示映射仅单向进行。

KotlinSwiftObjective-C备注
classclass@interface备注
interfaceprotocol@protocol
constructor/createInitializerInitializer备注
PropertyPropertyProperty备注 1备注 2
MethodMethodMethod备注 1备注 2
enum classclass@interface备注
suspend ->completionHandler:/ asynccompletionHandler:备注 1备注 2
@Throws funthrowserror:(NSError**)error备注
ExtensionExtensionCategory member备注
companion member <-Class method or propertyClass method or property
nullnilnil
Singletonshared or companion propertyshared or companion property备注
Primitive typePrimitive type / NSNumber备注
Unit return typeVoidvoid
StringStringNSString
StringNSMutableStringNSMutableString备注
ListArrayNSArray
MutableListNSMutableArrayNSMutableArray
SetSetNSSet
MutableSetNSMutableSetNSMutableSet备注
MapDictionaryNSDictionary
MutableMapNSMutableDictionaryNSMutableDictionary备注
Function typeFunction typeBlock pointer type备注
Inline classesUnsupportedUnsupported备注

类 (Classes)

名称转换

Objective-C 类以其原始名称导入到 Kotlin 中。 协议作为带有 Protocol 名称后缀的接口导入,例如,@protocol Foo -> interface FooProtocol。 这些类和接口放置在构建配置中指定的包中 (预配置的系统框架的 platform.* 包)。

Kotlin 类和接口的名称在导入到 Objective-C 时会添加前缀。 该前缀派生自框架名称。

Objective-C 不支持框架中的包。如果 Kotlin 编译器在同一框架中找到具有相同名称但不同包的 Kotlin 类,它会重命名它们。 此算法尚不稳定,并且可能在 Kotlin 版本之间发生更改。要解决此问题,您可以在框架中重命名冲突的 Kotlin 类。

强链接

每当您在 Kotlin 源代码中使用 Objective-C 类时,它都会被标记为强链接符号。 生成的构建工件将相关符号标记为强外部引用。

这意味着应用程序尝试在启动期间动态链接符号,如果这些符号不可用,则应用程序会崩溃。 即使从未使用的符号,也会发生崩溃。符号可能在特定设备或操作系统版本上不可用。

要解决此问题并避免“Symbol not found”错误,请使用 Swift 或 Objective-C 包装器来检查该类是否实际可用。 请参阅 Compose Multiplatform 框架中如何实现此解决方法

初始值设定项 (Initializers)

Swift/Objective-C 初始值设定项作为构造函数或名为 create 的工厂方法导入到 Kotlin 中。 后者发生在 Objective-C 类别中声明的初始值设定项或作为 Swift 扩展的情况下,因为 Kotlin 没有扩展构造函数的概念。

在将 Swift 初始值设定项导入到 Kotlin 之前,请不要忘记使用 @objc 对其进行注解。

Kotlin 构造函数作为初始值设定项导入到 Swift/Objective-C 中。

Setters

覆盖超类的只读属性的可写 Objective-C 属性表示为属性 foosetFoo() 方法。 协议的可变属性的只读属性也是如此。

顶层函数和属性 (Top-level functions and properties)

顶层 Kotlin 函数和属性可以作为特殊类的成员访问。 每个 Kotlin 文件都会转换为这样的类,例如:

// MyLibraryUtils.kt
package my.library

fun foo() {}

然后,您可以像这样从 Swift 调用 foo() 函数:

MyLibraryUtilsKt.foo()

请参阅 Kotlin-Swift interopedia 中访问顶层 Kotlin 声明的示例集合:

方法名称转换

通常,Swift 参数标签和 Objective-C 选择器片段映射到 Kotlin 参数名称。 这两个概念具有不同的语义,因此有时 Swift/Objective-C 方法可以导入为具有冲突的 Kotlin 签名。 在这种情况下,可以使用命名参数从 Kotlin 调用冲突的方法,例如:

[player moveTo:LEFT byMeters:17]
[player moveTo:UP byInches:42]

在 Kotlin 中,它是:

player.moveTo(LEFT, byMeters = 17)
player.moveTo(UP, byInches = 42)

以下是如何将 kotlin.Any 函数映射到 Swift/Objective-C:

KotlinSwiftObjective-C
equals()isEquals(_:)isEquals:
hashCode()hashhash
toString()descriptiondescription

请参阅 Kotlin-Swift interopedia 中数据类的示例

您可以在 Swift 或 Objective-C 中指定更符合语言习惯的名称,而不是使用 @ObjCName 注解 重命名 Kotlin 声明。

错误和异常

所有 Kotlin 异常都是未经检查的,这意味着在运行时捕获错误。 但是,Swift 只有在编译时处理的已检查错误。 因此,如果 Swift 或 Objective-C 代码调用一个抛出异常的 Kotlin 方法,则应使用 @Throws 注解标记 Kotlin 方法,指定“预期”异常类的列表。

编译到 Objective-C/Swift 框架时,具有或继承 @Throws 注解的非 suspend 函数在 Objective-C 中表示为 NSError*-producing 方法,在 Swift 中表示为 throws 方法。 suspend 函数的表示形式始终在 completion handler 中具有 NSError*/Error 参数。

当从 Swift/Objective-C 代码调用的 Kotlin 函数抛出异常,该异常是 @Throws 指定的类或其子类的实例时,它将作为 NSError 传播。 到达 Swift/Objective-C 的其他 Kotlin 异常被视为未处理并导致程序终止。

没有 @Throwssuspend 函数仅传播 CancellationException(作为 NSError)。 没有 @Throws 的非 suspend 函数根本不传播 Kotlin 异常。

请注意,尚未实现相反的反向转换:Swift/Objective-C 抛出错误的方法不会作为抛出异常导入到 Kotlin。

请参阅 Kotlin-Swift interopedia 中的示例

枚举 (Enums)

Kotlin 枚举作为 @interface 导入到 Objective-C 中,作为 class 导入到 Swift 中。 这些数据结构具有与每个枚举值对应的属性。考虑以下 Kotlin 代码:

// Kotlin
enum class Colors {
RED, GREEN, BLUE
}

您可以按如下方式从 Swift 访问此枚举类的属性:

// Swift
Colors.red
Colors.green
Colors.blue

要在 Swift switch 语句中使用 Kotlin 枚举的变量,请提供一个 default 语句以防止编译错误:

switch color {
case .red: print("It's red")
case .green: print("It's green")
case .blue: print("It's blue")
default: fatalError("No such color")
}

请参阅 Kotlin-Swift interopedia 中的另一个示例

挂起函数 (Suspending functions)

从 Swift 代码调用 suspend 函数作为 async 的支持是 实验性的。 它可能随时被删除或更改。 仅将其用于评估目的。我们感谢您在 YouTrack 中提供有关它的反馈。

Kotlin 的挂起函数 (suspend) 在生成的 Objective-C 标头中表示为带有回调的函数,或者在 Swift/Objective-C 术语中表示为 completion handlers

从 Swift 5.5 开始,Kotlin 的 suspend 函数也可用于从 Swift 调用,作为不使用 completion handlers 的 async 函数。 当前,此功能是高度实验性的,并且具有某些限制。有关详细信息,请参见 此 YouTrack 问题

扩展和类别成员 (Extensions and category members)

Objective-C 类别和 Swift 扩展的成员通常作为扩展导入到 Kotlin 中。 这就是为什么这些声明无法在 Kotlin 中重写,并且扩展初始值设定项不可用作 Kotlin 构造函数。

目前,有两个例外。从 Kotlin 1.8.20 开始,在与 NSView 类(来自 AppKit 框架)或 UIView 类(来自 UIKit 框架)相同的标头中声明的类别成员将作为这些类的成员导入。 这意味着您可以重写从 NSView 或 UIView 继承的方法。

Kotlin 对“常规”Kotlin 类的扩展分别作为扩展和类别成员导入到 Swift 和 Objective-C。 Kotlin 对其他类型的扩展被视为带有附加接收器参数的顶层声明。这些类型包括:

  • Kotlin String 类型
  • Kotlin 集合类型和子类型
  • Kotlin interface 类型
  • Kotlin 原始类型
  • Kotlin inline
  • Kotlin Any 类型
  • Kotlin 函数类型和子类型
  • Objective-C 类和协议

请参阅 Kotlin-Swift interopedia 中的示例集合

Kotlin 单例 (Kotlin singletons)

Kotlin 单例(使用 object 声明,包括 companion object)作为具有单个实例的类导入到 Swift/Objective-C。

该实例通过 sharedcompanion 属性可用。

对于以下 Kotlin 代码:

object MyObject {
val x = "Some value"
}

class MyClass {
companion object {
val x = "Some value"
}
}

按如下方式访问这些对象:

MyObject.shared
MyObject.shared.x
MyClass.companion
MyClass.Companion.shared
备注

在 Objective-C 中通过 [MySingleton mySingleton] 访问对象以及在 Swift 中通过 MySingleton() 访问对象已被弃用。

请参阅 Kotlin-Swift interopedia 中的更多示例:

NSNumber

Kotlin 原始类型盒子映射到特殊的 Swift/Objective-C 类。例如,kotlin.Int 盒子在 Swift 中表示为 KotlinInt 类实例(或在 Objective-C 中表示为 ${prefix}Int 实例,其中 prefix 是框架名称前缀)。 这些类派生自 NSNumber,因此实例是支持所有相应操作的适当 NSNumber

NSNumber 类型用作 Swift/Objective-C 参数类型或返回值时,不会自动转换为 Kotlin 原始类型。 原因是 NSNumber 类型没有提供关于包装的原始值类型的足够信息,例如,NSNumber 在静态上未知是 ByteBoolean 还是 Double。 因此,Kotlin 原始值应手动强制转换为和从 NSNumber

NSMutableString

NSMutableString Objective-C 类在 Kotlin 中不可用。 当传递给 Kotlin 时,NSMutableString 的所有实例都会被复制。

集合 (Collections)

Kotlin 集合按 上表 中所述转换为 Swift/Objective-C 集合。 Swift/Objective-C 集合以相同的方式映射到 Kotlin,除了 NSMutableSetNSMutableDictionary

NSMutableSet 不会转换为 Kotlin MutableSet。要将对象传递给 Kotlin MutableSet,请显式创建这种 Kotlin 集合。 为此,请使用例如 Kotlin 中的 mutableSetOf() 函数或 Swift 中的 KotlinMutableSet 类以及 Objective-C 中的 ${prefix}MutableSetprefix 是框架名称前缀)。 对于 MutableMap 也是如此。

请参阅 Kotlin-Swift interopedia 中的示例

函数类型

Kotlin 函数类型对象(例如,lambdas)转换为 Swift 中的函数和 Objective-C 中的块。 请参阅 Kotlin-Swift interopedia 中带有 lambda 的 Kotlin 函数的示例

但是,在翻译函数和函数类型时,参数和返回值类型的映射方式存在差异。 在后一种情况下,原始类型映射到其盒装表示形式。Kotlin Unit 返回值在 Swift/Objective-C 中表示为相应的 Unit 单例。 可以像检索任何其他 Kotlin object 一样检索此单例的值。请参阅上表中的单例。

考虑以下 Kotlin 函数:

fun foo(block: (Int) `->` Unit) { ... }

它在 Swift 中表示如下:

func foo(block: (KotlinInt) `->` KotlinUnit)

您可以像这样调用它:

foo {
bar($0 as! Int32)
return KotlinUnit()
}

泛型 (Generics)

Objective-C 支持在类上定义的“轻量级泛型”,其功能集相对有限。Swift 可以导入在类上定义的泛型,以帮助向编译器提供其他类型信息。

Objective-C 和 Swift 的泛型功能支持与 Kotlin 不同,因此转换不可避免地会丢失一些信息,但是支持的功能保留了有意义的信息。

有关如何在 Swift 中使用 Kotlin 泛型的具体示例,请参见 Kotlin-Swift interopedia

限制

Objective-C 泛型不支持 Kotlin 或 Swift 的所有功能,因此在转换中会丢失一些信息。

泛型只能在类上定义,不能在接口(Objective-C 和 Swift 中的协议)或函数上定义。

可空性 (Nullability)

Kotlin 和 Swift 都将可空性定义为类型规范的一部分,而 Objective-C 在类型的属性和方法上定义可空性。因此,以下 Kotlin 代码:

class Sample<T>() {
fun myVal(): T
}

在 Swift 中看起来像这样:

class Sample<T>() {
fun myVal(): T?
}

为了支持潜在的可空类型,Objective-C 标头需要使用可空返回值定义 myVal

为了缓解这种情况,在定义泛型类时,如果泛型类型_永远不_应为 null,请提供非空类型约束:

class Sample<T : Any>() {
fun myVal(): T
}

这将强制 Objective-C 标头将 myVal 标记为不可为空。

变型 (Variance)

Objective-C 允许将泛型声明为协变或逆变。Swift 不支持变型。来自 Objective-C 的泛型类可以根据需要进行强制转换。

data class SomeData(val num: Int = 42) : BaseData()
class GenVarOut<out T : Any>(val arg: T)
let variOut = GenVarOut<SomeData>(arg: sd)
let variOutAny : GenVarOut<BaseData> = variOut as! GenVarOut<BaseData>

约束 (Constraints)

在 Kotlin 中,您可以为泛型类型提供上限。Objective-C 也支持此功能,但是在更复杂的情况下,该支持不可用,并且当前在 Kotlin - Objective-C 互操作中不受支持。这里的例外是不可为空的上限将使 Objective-C 方法/属性不可为空。

禁用 (To disable)

要使框架标头在没有泛型的情况下编写,请在您的构建文件中添加以下编译器选项:

binaries.framework {
freeCompilerArgs += "-Xno-objc-generics"
}

前向声明 (Forward declarations)

要导入前向声明,请使用 objcnames.classesobjcnames.protocols 包。例如,要导入在带有 library.package 的 Objective-C 库中声明的 objcprotocolName 前向声明,请使用一个特殊的前向声明包:import objcnames.protocols.objcprotocolName

考虑两个 objcinterop 库:一个使用 objcnames.protocols.ForwardDeclaredProtocolProtocol,另一个在另一个包中使用实际实现:

// First objcinterop library
#import <Foundation/Foundation.h>

@protocol ForwardDeclaredProtocol;

NSString* consumeProtocol(id<ForwardDeclaredProtocol> s) {
return [NSString stringWithUTF8String:"Protocol"];
}
// Second objcinterop library
// Header:
#import <Foundation/Foundation.h>
@protocol ForwardDeclaredProtocol
@end
// Implementation:
@interface ForwardDeclaredProtocolImpl : NSObject <ForwardDeclaredProtocol>
@end

id<ForwardDeclaredProtocol> produceProtocol() {
return [ForwardDeclaredProtocolImpl new];
}

要在两个库之间传输对象,请在您的 Kotlin 代码中使用显式 as 强制转换:

// Kotlin code:
fun test() {
consumeProtocol(produceProtocol() as objcnames.protocols.ForwardDeclaredProtocolProtocol)
}
备注

您只能从相应的实际类转换为 objcnames.protocols.ForwardDeclaredProtocolProtocol。 否则,您将收到一个错误。

映射类型之间的转换 (Casting between mapped types)

在编写 Kotlin 代码时,可能需要将对象从 Kotlin 类型转换为等效的 Swift/Objective-C 类型(或反之亦然)。 在这种情况下,可以使用普通的旧 Kotlin 强制转换,例如:

val nsArray = listOf(1, 2, 3) as NSArray
val string = nsString as String
val nsNumber = 42 as NSNumber

子类化 (Subclassing)

从 Swift/Objective-C 子类化 Kotlin 类和接口

Kotlin 类和接口可以被 Swift/Objective-C 类和协议子类化。

从 Kotlin 子类化 Swift/Objective-C 类和协议

可以使用 Kotlin final 类子类化 Swift/Objective-C 类和协议。尚不支持继承 Swift/Objective-C 类型的非 final Kotlin 类,因此无法声明继承 Swift/Objective-C 类型的复杂类层次结构。

可以使用 override Kotlin 关键字重写普通方法。在这种情况下,重写的方法必须与被重写的方法具有相同的参数名称。

有时需要重写初始值设定项,例如在子类化 UIViewController 时。作为 Kotlin 构造函数导入的初始值设定项可以被用 @OverrideInit 注解标记的 Kotlin 构造函数重写:

class ViewController : UIViewController {
@OverrideInit constructor(coder: NSCoder) : super(coder)

...
}

重写构造函数必须与被重写的构造函数具有相同的参数名称和类型。

要使用冲突的 Kotlin 签名重写不同的方法,您可以将 @ObjCSignatureOverride 注解添加到该类。 该注解指示 Kotlin 编译器忽略冲突的重载,以防从 Objective-C 类继承了多个具有相同参数类型但参数名称不同的函数。

默认情况下,Kotlin/Native 编译器不允许调用非指定的 Objective-C 初始值设定项作为 super() 构造函数。 如果指定的初始值设定项未在 Objective-C 库中正确标记,则此行为可能不方便。要禁用这些编译器检查,请将 disableDesignatedInitializerChecks = true 添加到库的 .def 文件

C 特性 (C features)

有关库使用一些普通 C 特性(例如,不安全指针、结构等)的示例案例,请参见 与 C 的互操作性

不支持 (Unsupported)

Kotlin 编程语言的某些功能尚未映射到 Objective-C 或 Swift 的相应功能。 当前,生成的框架标头中未正确公开以下功能:

  • 内联类(参数映射为底层原始类型或 id
  • 实现标准 Kotlin 集合接口(ListMapSet)和其他特殊类的自定义类
  • Objective-C 类的 Kotlin 子类