Привет, хабр!
Поддержка Android в Swift 6 и swift-android-sdk от finagolfin это настоящий прорыв, который наконец-то позволил мне выпустить JNIKit, который я разрабатывал для проекта SwifDroid ещё со времён Swift 5. Теперь мы можем просто import Android
, вместо того чтобы возиться с ручным импортом NDK header'ов, а сборка конечных бинарников теперь обеспечивается не отдельным тулчейном, а минималистичной SDK, которую в скором времени сделают официальной на Swift.org.
Сегодня я хочу показать как написать ваш первый Swift код для Android. Это будет увлекательное приключение, так что налейте чашечку чая и давайте начнём.
Что Вам Понадобится
Swift Stream IDE расширение для VSCode
Опционально, установленная Android Studio для тестирование библиотеки, которую мы разработаем, на реальном Android приложении.
Операционная система не важна, главное, чтобы она поддерживала Docker и VSCode.
Как только вы установили Docker, можете запускать VSCode.
Первым делом нужно установить расширение Dev Containers.

Далее нужно установить расширение Swift Stream IDE.

Создание Проекта
В левом меню VSCode кликните на иконку Swift Stream (с птичкой).

и смело жмите Start New Project
Теперь нужно ввести название проекта вашей Swift библиотеки

Вы можете видеть, что по умолчанию новый проект будет создан в вашей домашней папке. Это можно изменить выбрав иную папку нажав на три точки справа.
Теперь необходимо выбрать направление и тип проекта, Android -> Library
в нашем случае.

И нажимаем Create Project.
Теперь необходимо указать Java namespace вашей библиотеки. Обычно это доменное имя наоборот, к примеру com.example.mylib

Так же необходимо указать минимальную версию Android SDK

Я бы рекомендовал выбрать 24 или 29, в зависимости от ваших нужд. Жмите Enter, чтобы перейти к выбору версии Android SDK для компиляции

На сегодняшний день хорошим выбором будет 35. Жмите Enter ещё раз, чтобы запустить процесс создания проекта.
На этом моменте VSCode создаст папку проекта со всеми необходимыми файлами и начнёт скачивание Docker-образа с заранее подготовленным окружением для разработки под Android.
Как только образ загружен, VSCode откроет новое окно для вашего проекта, которое уже будет работать непосредственно внутри dev-контейнера. При первом запуске контейнер постарается загрузить актуальный Swift тулчейн, Swift for Android SDK, Gradle, Android NDK, т Android SDK. Все эти сущности будут заботливо использоваться из кэша, который организован на Docker Volumes. То есть создание следующего проекта будет происходить за секунды.
Первый запуск может занять продолжительное время в зависимости от скорости вашего интернета и мощности компьютера, так что наберитесь терпения и пока выпейте чаю.

И теперь всё готово! Можно начинать писать код!
Преамбула
Что такое JNI?
Java Native Interface (JNI) – это мост который позволяет нативному коду обращаться к Java Virtual Machine (JVM). Когда вы пишете Java код, то вы используете Android SDK. Но когда вы используете языки как Swift или C++, которые не компилируются в Java байткод, вам уже нужен Android NDK для коммуникации с Java через JNI.
Используя JNI вы можете делать практически всё то же самое, что и с Java, но делать это без головной боли это реальный челлендж.
Что такое JNIKit?
JNIKit вступает в игру как раз, чтобы убрать головную боль. Чтобы быть продуктивным и чувствовать себя комфортно на нужен удобный Swift-слой, который обернёт все низкоуровневые JNI вызовы созданные для C, во что-то намного более элегантное. Это то, для чего JNIKit был создан.
Проект
Структура
В сердце проекта конечно же Swift Package Manager. И ключевые зависимости: JNIKit
, и AndroidLogging
со swift-log
.
Ваш Swift код по умолчанию живёт в Sources/<target_name>/Library.swift
.
Android библиотека (Gradle проект) находится в папке Library
. Эта папка будет автоматически сгенерирована после первого сборки Swift кода. Альтернативно, вы можете запустить её генерацию слева на панели расширения Swift Stream.
Swift-код
Всё начинается с метода initialize
и он должен быть вызван один раз до вызова любых других нативных методов.
Этот метод экспортируется в JNI при помощи директивы @_cdecl
.
Имя метода очень критично, оно должно строго соответствовать JNI-паттерну
Java_<package>_<class>_<method>
package
это имя пакета содержащего класс, но вместо точек у него должны быть нижние подчеркиванияclass
это имя класса содержащего методmethod
это наименование метода
Аргументы метода так же следуют JNI-соглашению. Первые два обязательны и всегда передаются автоматически самим JNI:
envPointer
: Этот параметр никогда не меняется и представляет из себя указатель на JNI окружение, которое по факту является интерфейсом между нативным кодом и JVM.clazzRef
илиthizRef
: Здесь вы получаетеclazzRef
если ваш Java-метод статичный (как в нашем случае, где наш метод внутри Kotlinobject
). Вы так же можете получитьthizRef
если это метод инициализированного объекта. В первом случае это будет референс на класс, во втором референс на объект.
Все последующие аргументы представляют из себя параметры Java/Kotlin-метода. В нашем случае метод имеет один параметр: caller
. Мы передаём его из приложения в качестве контекста, чтобы, к примеру, взять из него class loader приложения (больше об этом далее). Если бы у нас был thizRef
, то он подошёл бы в качестве контекста и caller
можно было бы уже не передавать.
#if os(Android)
@_cdecl("Java_com_habr_swiftlib_myfirstandroidproject_SwiftInterface_initialize")
public func initialize(
envPointer: UnsafeMutablePointer<JNIEnv?>,
clazzRef: jobject,
callerRef: jobject
) {
// Активируем Android logger
LoggingSystem.bootstrap(AndroidLogHandler.taggedBySource)
// Инициализируем JVM
let jvm = envPointer.jvm()
JNIKit.shared.initialize(with: jvm)
// ДАЛЕЕ: кэшируем class loader
// ДАЛЕЕ: пример `toString`
// ДАЛЕЕ: пример `Task`
}
#endif
Тело метода показывает нам первичную конфигурацию swift-log
с Android Logger
, которую нужно делать только один раз.
Это даёт нам возможность использования логгера вывод которого будет доступен в LogCat.
let logger = Logger(label: "?? SWIFT")
logger.info("? Привет, Хабр!")
Далее в теле метода мы инициализировали подключение к JVM. Это тоже делается один раз.
На этом этапе наше приложение уже готово полноценно работать через JNI.
Class Loader и Кэш
С загрузкой классов можно легко попасть в ловушку ClassNotFoundException
потому что по умолчанию JNI использует системный загрузчик, который ничего не знает о динамически загруженных классах или зависимостях вашего приложения.
Решение? Нам нужен загрузчик из контекста приложения, который легко получить из абсолютно любого Java-объекта через .getClass().getClassLoader()
. Лучшей практикой является создание глобальной ссылки на объект загрузчика классоов в методе initialize
и сохранение его в JNICache, т.к. он остаётся валидным на всё время жизни приложения. Таким образом мы будем экономить JNI вызовы и получим прирост в скорости работы приложения.
Вот как мы можем кэшировать загрузчик классов в методе initialize
:
// Оборачиваем указатель на окружение
let localEnv = JEnv(envPointer)
// Конвертируем локальный референс объекта в глобальный
let callerBox = callerRef.box(localEnv)
// Defer-блок отработает для удаления локального референса
defer {
// Удаляем локальный референс на объект
localEnv.deleteLocalRef(callerRef)
}
// Создаём `JObject` из глобального референса на объект
guard let callerObject = callerBox?.object() else { return }
// Кэшируем загрузчик классов из caller-объекта
if let classLoader = callerObject.getClassLoader(localEnv) {
// Сохраняем его в специальный JNICache
JNICache.shared.setClassLoader(classLoader)
logger.info("? class loader cached successfully")
}
Если бы был
thizRef
, то можно было бы взять загрузчик классов из него
Могу ли я использовать toString()
?
Конечно, это классический Java-метод и для него сделана обёртка в JNIKit, а вызвать его можно на любом объект вот так вот просто:
logger.info("? caller description: \(someObject.toString())")
Использование окружения из другого потока
JNIEnv
жестко привязан к потоку. Оно является тем самым мостиком который делает всю магию JNI, передавая вызовы в JVM.
Если вы переключаете поток, например используя Task, вы должны подключиться к окружению текущего потока. JNIKit предоставляет простой метод для этого: JEnv.current()
.
Task {
// Access current environment in this thread
guard let env = JEnv.current() else { return }
logger.info("? new env: \(env)")
// Print JNI version into LogCat
logger.info("? jni version: \(env.getVersionString())")
}
Как выглядит код на другой стороне
Java
public final class SwiftInterface {
static {
System.loadLibrary("MyFirstAndroidProject");
}
private SwiftInterface() {}
public static native void initialize(Object caller);
}
Kotlin
object SwiftInterface {
init {
System.loadLibrary("MyFirstAndroidProject")
}
external fun initialize(caller: Any)
}
Swift Stream IDE генерирует Kotlin-файлы для вас, поэтому все дальнейшие примеры будут на Kotlin.
Сборка Swift-проекта
Отлично, время сбилдить проект! На боковой панели переключайтесь на таб Swift Stream
и нажимайте на Project -> Build
.

При первой сборке необходимо будет выбрать схему Debug
или Release
во всплывающем окне. В дальнейшем её можно изменить нажав на соответствующую кнопку Scheme
(на скриншоте выше).

В этот раз выберем Debug
и начнётся процесс сборки.
На боковой панели Swift Stream вы можете выбрать Log Level
, чтобы видеть больше или меньше деталей в процессе сборки:
Normal
Detailed (по умолчанию)
Verbose
Unbearable (когда вы хотите видеть абсолютно всё)
По умолчанию на Detailed
уровне во время сборки вы увидите следующий вывод в окне Output
:
?️ Started building debug
?♂️ it will try to build each phase
? Resolving Swift dependencies for native
? Resolved in 772ms
? Resolving Swift dependencies for droid
? Resolved in 2s918ms
? Building `MyFirstAndroidProject` swift target for arm64-v8a
? Built `MyFirstAndroidProject` swift target for `.droid` in 10s184ms
? Building `MyFirstAndroidProject` swift target for armeabi-v7a
? Built `MyFirstAndroidProject` swift target for `.droid` in 7s202ms
? Building `MyFirstAndroidProject` swift target for x86_64
? Built `MyFirstAndroidProject` swift target for `.droid` in 7s135ms
? Preparing gradle wrapper
? Prepared gradle wrapper in 1m50s
✅ Build Succeeded in 2m20s
Как вы можете видеть, Swift даже "на холодную" скомпилировался довольно быстро, всего ~30 секунд на все три таргета (arm64-v8a, armeabi-v7a, and x86_64). Большую часть времени (1 мин. 50 сек.) занял первый запуск команды gradle wrapper
, но это делается лишь изредка.
Отличная новость в том, что повторные запуски компиляции будут очень быстрыми, порядка 3 секунд на все три таргета! Это потому что уже всё в кэше.
Команда сборки так же автоматически генерирует Gradle-файлы для проекта Java-библиотеки для вас. Их теперь можно найти в папке Library
.
Java/Kotlin проект
Исходный код
Swift Stream генерирует начальный шаблонный код для вашей библиотеки, который вдальнейшем вы будете развивать и поддерживать самостоятельно.
import android.util.Log
object SwiftInterface {
init {
System.loadLibrary("MyFirstAndroidProject")
}
external fun initialize(caller: Any)
external fun sendInt(number: Int)
external fun sendIntArray(array: IntArray)
external fun sendString(string: String)
external fun sendDate(date: Date)
external fun ping(): String
external fun fetchAsyncData(): String
}
Gradle-файлы
Swift Stream не только генерирует Gradle-файлы, но и управляет ими в дальнейшем. Он также создаёт Gradle-таргет под каждый таргет из вашего Package.swift
и следит, чтобы Gradle-файлы были синхронизированы со Swift проектом.
В Library/settings.gradle.kts
, происходит управление списком таргетов внутри специальных тэгов:
// managed by swiftstreamide: includes-begin
include(":myfirstandroidproject")
// managed by swiftstreamide: includes-end
В каждом Library/<target>/build.gradle.kts
файле, происходит автоматическое управление зависимостями необходимыми для корректной работы Swift кода в этом таргете. runtime-libs:core
является обязательной зависимостью, а остальные runtime-libs:
внутри специальных тегов могут меняться в зависимости от вашего Swift кода:
implementation("com.github.swifdroid.runtime-libs:core:6.1.3")
// managed by swiftstreamide: so-dependencies-begin
implementation("com.github.swifdroid.runtime-libs:foundation:6.1.3")
implementation("com.github.swifdroid.runtime-libs:foundationessentials:6.1.3")
implementation("com.github.swifdroid.runtime-libs:i18n:6.1.3")
// managed by swiftstreamide: so-dependencies-end
По умолчанию, эти зависимости подтягиваются автоматически из SwiftDroid runtime-libs
репозитория на JitPack, и репозиторий обновляется вместе с выходом новой версии Swift. Это означает, что вам не нужно возиться с ручным копированием .so файлов из Android SDK бандла!
Если вам всё-таки нужно больше контроля, то вы можете включить ручной режим, но всё ещё без ручного копирования каждого файла. Swift Stream IDE использует конфигурационный файл (.vscode/android-stream.json
) в котором вы можете переключить режим soMode
:
"soMode": "Packed"
"Packed"
выбран по умолчанию и он означает, что Gradle импортирует всё из JitPack. Вы можете переключиться на "PickedManually"
и перечислить только нужные вам .so
файлы:
"soMode": "PickedManually",
"schemes": [
{
"title": "MyFirstAndroidProject Debug",
"soFiles": [
"libandroid.so",
"libc.so",
"libm.so"
]
}
]
В этом же конфигурационном файле вы можете изменить и другие настройки проекта:
"packageName": "com.habr.swiftlib",
"compileSDK": 35,
"minSDK": 24,
"javaVersion": 11,
А ещё здесь же вы можете передать кастомные аргументы для команды swift build
:
"schemes": [
{
"title": "MyFirstAndroidProject Debug",
"swiftArgs": []
}
]
Сборка Gradle проекта
Наконец-то мы добрались до сборки Android-библиотеки в файл .aar
, для этого в боковой панели Swift Stream выберите Java Library Project -> Assemble
.

Эта кнопка запустит либо команду gradlew assembleDebug
или gradlew assembleRelease
, которая сформирует вашу библиотеку для локального использования или дальнейшей публикации.
Добавим эту библиотеку в Android проект (локально)
Это самая веселая часть, т.к. мы увидим работу свифтв в вашем реальном Android приложении! Откройте существующий или создайте новый проект в Android Studio.
Как только вы в проекте, первым шагом нужно добавить JitPack в качестве репозитория. Найдите ваш settings.gradle.kts
и убедитесь, что вы подключили JitPack:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
maven { url = uri("https://jitpack.io") } // <-- это оно
mavenCentral()
}
}
Далее нужно прописать зависимости в build.gradle.kts
модуля вашего приложения (это может быть app/build.gradle.kts
). Здесь нужно подключить и ваш .aar файл и все необходимые свифту рантайм-зависимости:
dependencies {
implementation(files("libs/myfirstandroidproject-debug.aar"))
implementation("com.github.swifdroid.runtime-libs:core:6.1.3")
implementation("com.github.swifdroid.runtime-libs:foundation:6.1.3")
implementation("com.github.swifdroid.runtime-libs:foundationessentials:6.1.3")
implementation("com.github.swifdroid.runtime-libs:i18n:6.1.3")
// the rest of dependencies
}
Важно: Вам необходимо вручную прописывать здесь
runtime-libs:
зависимости потому что Gradle не может получить их из.aar
файла. Но такого не будет если вы будете распространять свою библиотеку через репозиторий.
Получаем .AAR файл
После сборки Gradle проекта (выше) у нас получился .aar
файл библиотеки, который вы сможете найти по адресу:
Library/myfirstandroidproject/build/outputs/aar/myfirstandroidproject-debug.aar
Скопируйте этот файл. Далее в папке вашего Android приложения перейдите в папку модуля (например, app/
) и создайте там папку libs
(она должна расположиться там же где лежит build.gradle.kts
. Вставьте ваш .aar
файл в папку libs
.
И начнется магия! ?
Отлично, у нас всё есть! Теперь в коде вашего проекта, где-нибудь в классе Application
или в методе onCreate
вашей Activity
, инициализируйте Swift:
SwiftInterface.initialize(this)
Синхронизируйте Gradle и запускайте ваш Android проект на устройстве или эмуляторе.
Момент истины: откройте LogCat и отфильтруйте по слову "SWIFT". Вы должны увидеть это славное послание:
I [?? SWIFT] ? Привет, Хабр!
Урра!!! Ваш Swift код запустился на Android!
Дальнейшие итерации разработки
Когда вы делаете изменения в вашем Swift код, вам придётся проделать следующее:
На панели Swift Stream нажать
Project -> Build
Далее, нажать
Java Library Project -> Assemble
Скопировать новый
.aar
файл из папкиoutputs/aar
в папку вашего Android проекта (app/libs
), перезаписав старый файл
Вот и всё! Теперь вы кроссплатформенный Swift-разработчик!
Пример работы с JNI
Переходим к самой интересной части, к коду! Поговорим о том как можно коммуницировать между Swift и Java/Kotlin. Как я говорил выше, мы будем всё писать на Kotlin, просто потому что это стандарт для Android разработчиков в наши дни.
Мы покроем некоторые простые, но самые насущные сценарии в этот раз, а погрузимся глубже в следующих публикациях.
⚠️ Напоминание: необходимо вызвать
SwiftInterface.initialize(this)
перед вызовом других нативных методов!
Отправляем Int из Kotlin в Swift
Начнём с простого. Задекларируйте метод в SwiftInterface.kt
:
external fun sendInt(number: Int)
Со стороны свифта имплементируйте следующее:
#if os(Android)
@_cdecl("Java_com_habr_swiftlib_myfirstandroidproject_SwiftInterface_sendInt")
public func sendInt(
envPointer: UnsafeMutablePointer<JNIEnv?>,
clazzRef: jobject,
number: jint
) {
let logger = Logger(label: "?? SWIFT")
logger.info("#️⃣ sendInt: \(number)")
}
#endif
Вызовите его из вашего приложения:
SwiftInterface.sendInt(123)
Проверьте LogCat:
I [?? SWIFT] #️⃣ sendInt: 123
Это было просто, да? :)
Отправляем IntArray из Kotlin в Swift
Задекларируйте метод:
external fun sendIntArray(array: IntArray)
Со стороны свифта принимайте массив:
#if os(Android)
@_cdecl("Java_com_habr_swiftlib_myfirstandroidproject_SwiftInterface_sendIntArray")
public func sendIntArray(
envPointer: UnsafeMutablePointer<JNIEnv?>,
clazzRef: jobject,
arrayRef: jintArray
) {
// Создаём легковесный логгер
let logger = Logger(label: "?? SWIFT")
// Получаем доступ к текущему окружению
let localEnv = JEnv(envPointer)
// Defer-блок отработает для удаления локального референса
defer {
// Удаляем локальный референс на объект
localEnv.deleteLocalRef(arrayRef)
}
// Получаем длину массива
logger.info("? sendIntArray 1")
let length = localEnv.getArrayLength(arrayRef)
logger.info("? sendIntArray 2 length: \(length)")
// Получаем элементы массива
var swiftArray = [Int32](repeating: 0, count: Int(length))
localEnv.getIntArrayRegion(arrayRef, start: 0, length: length, buffer: &swiftArray)
// Теперь можно использовать `swiftArray` как обычную Swift array
logger.info("? sendIntArray 3 swiftArray: \(swiftArray)")
}
#endif
Вызовите из вашего приложения:
SwiftInterface.sendIntArray(intArrayOf(7, 6, 5))
Проверьте LogCat:
I [?? SWIFT] ? sendIntArray: 1
I [?? SWIFT] ? sendIntArray: 2 length: 3
I [?? SWIFT] ? sendIntArray: 3 swiftArray: [7, 6, 5]
Отправляем String из Kotlin в Swift
Задекларируйте метод:
external fun sendString(string: String)
Со стороны свифта:
#if os(Android)
@_cdecl("Java_com_habr_swiftlib_myfirstandroidproject_SwiftInterface_sendString")
public func sendString(envPointer: UnsafeMutablePointer<JNIEnv?>, clazzRef: jobject, strRef: jobject) {
// Создаём легковесный логгер
let logger = Logger(label: "?? SWIFT")
// Получаем доступ к текущему окружению
let localEnv = JEnv(envPointer)
// Defer-блок отработает для удаления локального референса
defer {
// Удаляем локальный референс на объект
localEnv.deleteLocalRef(strRef)
}
// Оборачиваем JNI string референс в `JString` объект
// и получаем нативную Swift строку
logger.info("✍️ sendString 1")
guard let string = strRef.wrap().string() else {
logger.info("✍️ sendString 1.1 exit: unable to unwrap jstring")
return
}
// Используем нативную строку как только пожелаем
logger.info("✍️ sendString 2: \(string)")
}
#endif
Вызовите из вашего приложения:
SwiftInterface.sendString("С любовью из Java")
Проверьте LogCat:
I [?? SWIFT] ✍️ sendString 1
I [?? SWIFT] ✍️ sendString 2: С любовью из Java
Отправляем Date объект из Kotlin в Swift
Задекларируйте метод:
external fun sendDate(date: Date)
Со стороны свифта:
#if os(Android)
@_cdecl("Java_com_habr_swiftlib_myfirstandroidproject_SwiftInterface_sendDate")
public func sendDate(envPointer: UnsafeMutablePointer<JNIEnv?>, clazzRef: jobject, dateRef: jobject) {
// Создаём легковесный логгер
let logger = Logger(label: "?? SWIFT")
// Получаем доступ к текущему окружению
let localEnv = JEnv(envPointer)
// Defer-блок отработает для удаления локального референса
defer {
// Удаляем локальный референс на объект
localEnv.deleteLocalRef(dateRef)
}
// Конвертируем локальный референс на JNI date объект в глобальный
logger.info("? sendDate 1")
guard let box = dateRef.box(localEnv) else {
logger.info("? sendDate 1.1 exit: unable to box Date object")
return
}
// Создаём `JObject` из глобального референса на date объект
logger.info("? sendDate 2")
guard let dateObject = box.object() else {
logger.info("? sendDate 2.1 exit: unable to unwrap Date object")
return
}
// Вызываем метод `getTime` на объекте и получаем миллисекунды с начала эпохи
logger.info("? sendDate 3")
guard let milliseconds = dateObject.callLongMethod(name: "getTime") else {
logger.info("? sendDate 3.1 exit: getTime returned nil, maybe wrong method")
return
}
// И распоряжаемся миллисекундами как только пожелаем :)
logger.info("? sendDate 4: \(milliseconds)")
}
#endif
Вызовите из вашего приложения:
SwiftInterface.sendDate(Date())
Проверьте LogCat:
I [?? SWIFT] ? sendDate 1
I [?? SWIFT] ? sendDate 2
I [?? SWIFT] ? sendDate 3
I [?? SWIFT] ? sendDate 4: 1757533833096
Получаем Date объект из Kotlin в Swift
Задекларируйте метод:
external fun sendDate(date: Date)
Со стороны свифта напишем метод возвращающий строку:
#if os(Android)
@_cdecl("Java_com_habr_swiftlib_myfirstandroidproject_SwiftInterface_ping")
public func ping(envPointer: UnsafeMutablePointer<JNIEnv?>, clazzRef: jobject) -> jobject? {
// Обернём Swift строку в `JSString` вернём её JNI референс
return "? Pong from Swift!".wrap().reference()
}
#endif
Вызовите из вашего приложения:
Log.i("HELLO", "Pinging: ${SwiftInterface.ping()}")
Проверьте LogCat:
I Pinging: ? Pong from Swift!
Исполняем асинхронный Swift код из Kotlin
Задекларируйте метод:
external fun fetchAsyncData(): String
Здесь важно знать, что оператор @_cdecl
не поддерживает асинхронные методы. Поэтому мы будем использовать семафор вокруг асинхронной задачи. Этот подход работает в случае вызова метода с семафором из не-UI потока, в противном случае вы столкнетесь с дедлоком. Мы разберемся как иметь дело с UI в следующих публикациях.
#if os(Android)
@_cdecl("Java_com_habr_swiftlib_myfirstandroidproject_SwiftInterface_fetchAsyncData")
public func fetchAsyncData(
env: UnsafeMutablePointer<JNIEnv>,
obj: jobject
) -> jstring? {
// Создаём семафор, чтобы ожидать асинхронный таск
let semaphore = DispatchSemaphore(value: 0)
// Создаём переменную для результата
var result: String? = nil
// Стартуем асинхронный таск
Task {
// Симулируем длительную асинхронную операцию
try? await Task.sleep(nanoseconds: 5_000_000_000) // 5 seconds
// Записываем результат в переменную
result = "Async data fetched successfully!"
// Отпускаем семафор
semaphore.signal()
}
// Блокируем текущий поток семафором в ожидании результата
semaphore.wait()
// Проверяем доступен ли результат
guard let result = result else { return nil }
// Оборачиваем строку в `JSString` и возвращаем её JNI референс
return result.wrap().reference()
}
#endif
Вызовите из вашего приложения (не на UI потоке):
CoroutineScope(Dispatchers.IO).launch {
Log.i("ASYNC", "Swift async call started")
try {
val result = SwiftInterface.fetchAsyncData()
Log.i("ASYNC", "Swift returned: $result")
} catch (e: Exception) {
// Handle error
}
Log.i("ASYNC", "Swift async call finished")
}
Проверьте LogCat:
I Swift async call started
I Swift returned: Async data fetched successfully!
I Swift async call finished
Обернём Java класс в Swift
Чтобы использовать какой-либо Java класс удобно, нам нужно написать для него обёртку. Давайте разберёмся как это сделать на примере java/util/Date
:
public final class JDate: JObjectable, Sendable {
/// Название JNI класса
public static let className: JClassName = "java/util/Date"
/// Обёртка над глобальным JNI рефернсом, содержит в себе также метаданные класса.
public let object: JObject
/// Конструктор для ситуации когда вы уже имеете `JObject`.
///
/// Это может быть полезно когда вы получаете `Date` объект из Java.
public init (_ object: JObject) {
self.object = object
}
/// Этот конструктор создаёт `Date` объект в JNI без аргументов
public init? () {
#if os(Android)
guard
// Получаем доступ к текущему окружению
let env = JEnv.current(),
// Находим класс `java.util.Date`
// и загружаем его напрямую или из кэша
let clazz = JClass.load(Self.className),
// Вызываем создание нового объекта `java.util.Date` в JNI
// и получаем его глобальный референс
let global = clazz.newObject(env)
else { return nil }
// Сохраняем объект, чтобы использовать его в методах
self.object = global
#else
// Для не-Android платформ возвращаем nil
return nil
#endif
}
/// Этот конструктор создаёт `Date` объект в JNI с аргументом
public init? (_ milliseconds: Int64) {
#if os(Android)
guard
// Получаем доступ к текущему окружению
let env = JEnv.current(),
// Находим класс `java.util.Date`
// и загружаем его напрямую или из кэша
let clazz = JClass.load(Self.className),
// Вызываем создание нового объекта `java.util.Date` в JNI,
// передавая в него параметр с миллисекундами,
// и получаем его глобальный референс
let global = clazz.newObject(env, args: milliseconds)
else { return nil }
// Сохраняем объект, чтобы использовать его в методах
self.object = global
#else
// Для не-Android платформ возвращаем nil
return nil
#endif
}
}
Выше показан необходимый минимум, чтобы начать работать с этим классом. Этот код даёт вам инициализировать java.util.Date
объект из ничего или обернуть уже имеющийся референс.
Окей, скелет у нас есть. Теперь ему нужны мышцы, давайте напишем ему некоторые методы!
/// Returns the day of the week represented by this date.
public func day() -> Int32? {
// Удобный вызов `java.util.Date.getDay()`
object.callIntMethod(name: "getDay")
}
Уверен, что вы уловили идею! Теперь сами напишите аналогичным образом методы getHours
, getMinutes
, getSeconds
и getTime
.
Немного иной метод с входящим параметром мы снова напишем вместе:
/// Tests if this date is before the specified date.
public func before(_ date: JDate) -> Bool {
// Удобный вызов `java.util.Date.before(Date date)`
// который принимает `Date` объект
// и возвращает boolean результат
object.callBoolMethod(name: "before", args: date.object.signed(as: JDate.className)) ?? false
}
И, как вы уже догадались, следующий метод after
вы напишете самостоятельно по аналогии с before
.
И, полагаю, чтобы нам сделать наш класс действительно полезным нам пригодится метод возвращающий Swift Date
:
/// Converts this java `Date` object to a Swift `Date`.
public func date() -> Date? {
// Получим миллисекунды с начала эпохи методом `getTime`
guard let time = time() else { return nil }
// Конвертируем миллисекунды в секунды и создаём Swift `Date` объект
return Date(timeIntervalSince1970: TimeInterval(time) / 1000.0)
}
Теперь у вас есть базовое понимание того как Swift работает с Java/Kotlin через JNI! Я надеюсь, что вам удалось успешно скомпилировать и протестировать код в вашем Android проекте.
Это всё на сегодня, друзья.
Для более глубокого погружения в мир JNI вы можете ознакомиться с детальным README в репозитории JNIKit на Github.
Будем рады вам в нашем комьюнити в Telegram и Discord, не стесняйтесь задавать вопросы!
Подпишитесь, чтобы не пропустить следующие публикации! Мы обязательно поговорим о том как опубликовать вашу Swift библиотеку на JitPack, погрузимся в более сложные JNI случаи, и... конечно же построим UI!
До связи!