Взаимодействие с Swift/Objective-C
В этом документе описываются некоторые детали взаимодействия Kotlin/Native со Swift/Objective-C.
Использование
Kotlin/Native обеспечивает двустороннее взаимодействие с Objective-C. Objective-C фреймворки и библиотеки могут быть использованы в Kotlin коде, если правильно импортированы в сборку (системные фреймворки импортируются по умолчанию). Более подробную информацию см. в конфигурациях компиляции. Библиотека Swift может быть использована в Kotlin коде, если ее API экспортирован в Objective-C с @objc. Чистые Swift модули пока не поддерживаются.
Kotlin модули могут быть использованы в Swift/Objective-C коде, если скомпилированы в фреймворк (см. здесь, как объявлять двоичные файлы). Пример см. в примере Kotlin Multiplatform Mobile.
Сопоставления
Таблица ниже показывает, как концепции Kotlin сопоставляются со Swift/Objective-C и наоборот.
"->" и "<-" указывают на то, что сопоставление происходит только в одном направлении.
Kotlin |
Swift |
Objective-C |
Примечания |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
Initializer |
Initializer |
|
Property |
Property |
Property |
|
Method |
Method |
Method |
|
|
|
|
|
|
|
|
|
Extension |
Extension |
Category member |
|
|
Class method or property |
Class method or property |
|
|
|
|
|
|
|
|
|
Примитивный тип |
Примитивный тип / |
||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Function type |
Function type |
Block pointer type |
|
Inline classes |
Unsupported |
Unsupported |
Перевод имен
Классы Objective-C импортируются в Kotlin с их исходными именами. Протоколы импортируются как интерфейсы с суффиксом Protocol, например @protocol Foo-> interface FooProtocol. Эти классы и интерфейсы помещаются в пакет, указанный в конфигурации сборки (пакеты platform.* для предварительно настроенных системных фреймворков).
Имена классов и интерфейсов Kotlin добавляются в начале при импорте в Objective-C. Префикс формируется из имени фреймворка.
Objective-C не поддерживает пакеты во фреймворке. Таким образом, компилятор Kotlin переименовывает классы Kotlin, которые имеют одинаковое имя, но разные пакеты в одном и том же фреймворке. Этот алгоритм пока нестабилен и может меняться между выпусками Kotlin. В качестве обходного пути вы можете переименовать конфликтующие классы Kotlin во фреймворке.
Инициализаторы
Инициализаторы Swift/Objective-C импортируются в Kotlin как конструкторы и фабричные методы с именем create. Последнее происходит с инициализаторами, объявленными в категории Objective-C или как расширение Swift, поскольку в Kotlin нет понятия расширенных конструкторов.
Конструкторы Kotlin импортируются как инициализаторы в Swift/Objective-C.
Установщики
Записываемые свойства Objective-C, переопределяющие свойства только для чтения суперкласса, представлены как метод setFoo() для свойства foo. То же самое относится к свойствам только для чтения протокола, которые реализованы как изменяемые.
Функции и свойства верхнего уровня
Функции и свойства верхнего уровня Kotlin доступны как члены специальных классов. Каждый файл Kotlin преобразуется в такой класс. Например:
// MyLibraryUtils.kt
package my.library
fun foo() {}
можно вызвать из Swift как
MyLibraryUtilsKt.foo()
Перевод имен методов
Как правило, метки аргументов 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 (equals(), hashCode() и toString()) сопоставляются с методами isEquals:, hash и description в Objective-C и с методом isEquals(_:) и свойствами hash, description в Swift.
Ошибки и исключения
В Kotlin нет понятия проверенных исключений, все исключения Kotlin являются непроверенными. В Swift есть только проверяемые ошибки. Поэтому, если код Swift или Objective-C вызывает метод Kotlin, который генерирует исключение для обработки, то метод Kotlin должен быть помечен аннотацией @Throws, указывающей список "ожидаемых" классов исключений.
При компиляции во фреймворк Objective-C/Swift, функции, не являющиеся suspend, имеющие или наследующие аннотацию @Throws, представлены как методы, генерирующие NSError*, в Objective-C и как методы throws в Swift. Представления для функций suspend всегда имеют параметр NSError*/Error в обработчике завершения.
Когда функция Kotlin, вызываемая из кода Swift/Objective-C, генерирует исключение, которое является экземпляром одного из классов, указанных в @Throws, или их подклассов, оно распространяется как NSError. Другие исключения Kotlin, достигающие Swift/Objective-C, считаются необработанными и приводят к завершению программы.
Функции suspend без @Throws распространяют только CancellationException как NSError. Функции, не являющиеся suspend, без @Throws вообще не распространяют исключения Kotlin.
Обратите внимание, что обратное преобразование пока не реализовано: методы Swift/Objective-C, генерирующие ошибки, не импортируются в Kotlin как генерирующие исключения.
Функции приостановления
Функции приостановления Kotlin (suspending functions) (suspend) представлены в сгенерированных заголовках Objective-C как функции с обратными вызовами или обработчиками завершения в терминах Swift/Objective-C.
Начиная с Swift 5.5, функции Kotlin suspend также доступны для вызова из Swift как функции async без использования обработчиков завершения. В настоящее время эта функциональность находится на стадии активного эксперимента и имеет определённые ограничения. Подробности см. в этом вопросе YouTrack.
Узнайте больше о async/await механизме в Swift.
Расширения и члены категорий
Члены Objective-C категорий и Swift расширений импортируются в Kotlin как расширения. Вот почему эти объявления нельзя переопределять в Kotlin. И инициализаторы расширений недоступны как конструкторы Kotlin.
Расширения Kotlin для «обычных» классов Kotlin импортируются в Swift и Objective-C как расширения и члены категорий соответственно. Расширения Kotlin для других типов рассматриваются как глобальные объявления с дополнительным параметром получателя. К этим типам относятся:
Тип Kotlin
StringТипы Kotlin коллекций и их подтипы
Типы Kotlin
interfaceПримитивные типы Kotlin
Классы Kotlin
inlineТип Kotlin
AnyТипы функций Kotlin и их подтипы
Классы и протоколы Objective-C
Kotlin синглтоны
Синглтон Kotlin (созданный с помощью объявления object, включая companion object) импортируется в Swift/Objective-C как класс с единственным экземпляром.
Экземпляр доступен через свойства shared и companion.
Для следующего кода 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
NSNumber
Контейнеры примитивных типов Kotlin отображаются на специальные классы Swift/Objective-C. Например, контейнер типа kotlin.Int представлен экземпляром класса KotlinInt в Swift (или экземпляром класса ${prefix}Int в Objective-C, где prefix — префикс имён фреймворков). Эти классы являются производными от NSNumber, поэтому экземпляры являются полноценными NSNumber с поддержкой всех соответствующих операций.
Тип NSNumber не преобразуется автоматически в примитивные типы Kotlin, когда используется в качестве типа параметра или возвращаемого значения Swift/Objective-C. Причина в том, что тип NSNumber не предоставляет достаточной информации о типе оборачиваемого примитивного значения, т. е. NSNumber статически неизвестно, является ли оно, например, Byte, Boolean или Double. Поэтому примитивные значения Kotlin должны быть преобразованы в/из NSNumber вручную (см. ниже).
NSMutableString
Класс Objective-C NSMutableString недоступен из Kotlin. Все экземпляры NSMutableString копируются при передаче в Kotlin.
Коллекции
Kotlin коллекции преобразуются в Swift/Objective-C коллекции, как описано в таблице выше. Swift/Objective-C коллекции отображаются на Kotlin аналогичным образом, за исключением NSMutableSet и NSMutableDictionary. NSMutableSet не преобразуется в Kotlin MutableSet. Чтобы передать объект для Kotlin MutableSet, вы можете явно создать эту Kotlin коллекцию, создав её в Kotlin, например, с помощью mutableSetOf(), или используя класс KotlinMutableSet в Swift (или ${prefix}MutableSet в Objective-C, где prefix — префикс имён фреймворков). То же самое относится к MutableMap.
Типы функций
Объекты типа Kotlin функций (например, лямбды) преобразуются в Swift функции/Objective-C блоки. Однако есть разница в том, как отображаются типы параметров и возвращаемых значений при переводе функции и типа функции. В последнем случае примитивные типы отображаются в их упакованном представлении. Возвращаемое значение Kotlin Unit представляется соответствующим Unit синглтоном в Swift/Objective-C. Значение этого синглтона можно получить так же, как и для любого другого Kotlin object (см. синглтоны в таблице выше). Подводя итог:
fun foo(block: (Int) -> Unit) { ... }
будет представлено в Swift как
func foo(block: (KotlinInt) -> KotlinUnit)
и может быть вызвано так
foo {
bar($0 as! Int32)
return KotlinUnit()
}
Обобщения
Objective-C поддерживает «лёгкие обобщения», определённые на классах, с относительно ограниченным набором функций. Swift может импортировать обобщения, определённые на классах, чтобы предоставить дополнительную информацию о типе компилятору.
Поддержка обобщений в Objective-C и Swift отличается от Kotlin, поэтому перевод неизбежно потеряет некоторую информацию, но поддерживаемые функции сохранят значимую информацию.
Ограничения
Objective-C обобщения не поддерживают все функции Kotlin или Swift, поэтому в переводе будет потеряна некоторая информация.
Обобщения могут быть определены только на классах, а не на интерфейсах (протоколах в Objective-C и Swift) или функциях.
Нуллируемость
Kotlin и Swift определяют нуллируемость как часть спецификации типа, а Objective-C определяет нуллируемость на методах и свойствах типа. Таким образом, следующее:
class Sample<T>() {
fun myVal(): T
}
будет (логически) выглядеть так:
class Sample<T>() {
fun myVal(): T?
}
Для поддержки потенциально нуллируемого типа заголовок Objective-C должен определить myVal с нуллируемым значением возврата.
Для решения этой проблемы, при определении своих обобщённых классов, если обобщённый тип никогда не должен быть нулём, укажите ограничение на ненулевой тип:
class Sample<T : Any>() {
fun myVal(): T
}
Это принудит заголовок Objective-C пометить myVal как не нулевой.
Изменчивость
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>
Ограничения
В Kotlin вы можете задавать верхние границы для обобщённого типа. Objective-C также поддерживает это, но эта поддержка недоступна в более сложных случаях и в настоящее время не поддерживается в межплатформенном взаимодействии Kotlin — Objective-C. Исключением является ситуация, когда верхняя граница не допускает null; в этом случае методы/свойства Objective-C будут не нулевыми.
Отключение
Чтобы получить заголовок фреймворка без обобщений, добавьте флаг в конфигурацию компилятора:
binaries.framework {
freeCompilerArgs += "-Xno-objc-generics"
}
Преобразование между отображёнными типами
При написании кода Kotlin может потребоваться преобразование объекта из типа Kotlin в эквивалентный тип Swift/Objective-C (или наоборот). В этом случае можно использовать обычное преобразование Kotlin, например,
val nsArray = listOf(1, 2, 3) as NSArray val string = nsString as String val nsNumber = 42 as NSNumber
Наследование
Наследование классов и интерфейсов Kotlin от Swift/Objective-C
Классы и интерфейсы Kotlin могут быть унаследованы классами и протоколами Swift/Objective-C.
Наследование классов и протоколов Swift/Objective-C из Kotlin
Классы и протоколы Swift/Objective-C могут быть унаследованы с помощью класса Kotlin final. Не-final классы Kotlin, наследующие типы Swift/Objective-C, пока не поддерживаются, поэтому невозможно объявить сложную иерархию наследования, включающую типы Swift/Objective-C.
Обычные методы могут быть переопределены с использованием ключевого слова Kotlin override. В этом случае метод-переопределение должен иметь те же имена параметров, что и переопределяемый.
Иногда требуется переопределять инициализаторы, например, при наследовании от UIViewController. Инициализаторы, импортированные как конструкторы Kotlin, могут быть переопределены конструкторами Kotlin, помеченными аннотацией @OverrideInit:
class ViewController : UIViewController {
@OverrideInit constructor(coder: NSCoder) : super(coder)
...
}
Конструктор-переопределение должен иметь те же имена и типы параметров, что и переопределяемый.
Для переопределения разных методов с конфликтующими подписями Kotlin можно добавить аннотацию @Suppress("CONFLICTING_OVERLOADS") к классу.
По умолчанию компилятор Kotlin/Native не позволяет вызывать неназначенный инициализатор Objective-C как конструктор super(...). Это поведение может быть неудобно, если назначенные инициализаторы не помечены должным образом в библиотеке Objective-C. Добавление аннотации disableDesignatedInitializerChecks = true в файл .def для этой библиотеки отключит эти проверки компилятора.
Особенности C
См. Взаимодействие с C для примера, где библиотека использует некоторые обычные возможности C, такие как небезопасные указатели, структуры и т. д.
Экспорт комментариев KDoc в сгенерированные заголовки Objective-C
По умолчанию, документационные комментарии KDocs не переводятся в соответствующие комментарии при генерации заголовка Objective-C.
Например, следующий код Kotlin с KDoc:
/** * Prints the sum of the arguments. * Properly handles the case when the sum doesn't fit in 32-bit integer. */ 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").kotlinOptions.freeCompilerArgs += "-Xexport-kdoc"
}
}
kotlin {
targets.withType(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget) {
compilations.get("main").kotlinOptions.freeCompilerArgs += "-Xexport-kdoc"
}
}
После этого заголовок Objective-C будет содержать соответствующий комментарий:
/**
* Prints the sum of the arguments.
* Properly handles the case when the sum doesn't fit in 32-bit integer.
*/
+ (void)printSumA:(int32_t)a b:(int32_t)b __attribute__((swift_name("printSum(a:b:)")));
Известные ограничения:
Документация зависимостей не экспортируется, если она не скомпилирована с
-Xexport-kdocсама. Эта функция экспериментальная, поэтому библиотеки, скомпилированные с этим флагом, могут быть несовместимы с другими версиями компилятора.Комментарии KDoc в основном экспортируются "как есть", многие функции KDoc (например,
@property) не поддерживаются.
Неподдерживаемое
Некоторые функции языка Kotlin пока не отображаются в соответствующих функциях Objective-C или Swift. В настоящее время следующие функции не правильно представлены в сгенерированных заголовках фреймворка:
inline классы (аргументы отображаются как базовый примитивный тип или
id)пользовательские классы, реализующие стандартные интерфейсы коллекций Kotlin (
List,Map,Set) и другие специальные классыKotlin подклассы Objective-C классов
© 2010–2022 JetBrains s.r.o. and Kotlin Programming Language contributors
Licensed under the Apache License, Version 2.0.
https://kotlinlang.org/docs/native-objc-interop.html