Bollywood HungamaAnushka Sen named Indian global ambassador for Newport Beach Film Festival 2026PunchChina pulls out of Sri Lanka port logistics ventureInquirer5 fishermen nabbed for alleged blast fishing in Occidental MindoroThe Jerusalem PostMassive overnight Russian attack leaves over half a million Ukrainians without power, Zelensky saysInquirer EntertainmentFilipino model Christina Nadin walks Victoria’s Secret Fashion Show 2026 runwayUN NewsProtecting Earth’s fragile biodiversity takes spotlight at Armenia talksCBS NewsCartel members linked to murders of 10 workers from Canadian companyVarietyBrit Coming-of-Age Comedy ‘The State of Us’ Lands U.K. Theatrical Deal With Vertigo (EXCLUSIVE)GIGAZINEMetaが「ニュースサイトからの情報収集を可能とする利用規約」を削除ZDF heuteEntdecken Sie das ZDF-NachrichtenstudioThe Sydney Morning HeraldPatterson hits 16th first-class tonObservador DesportoReitoria da Fernando Pessoa recusa regulamento sem pré-aviso
The Daily Newsstand · Free, Always
Monday, October 19, 2026

Swift на Android: путь к сингулярности. От анонса SwiftJava до полноценного приложения. Часть 2

Translate

Всем привет! С вами Анна Жаркова, руководитель практики мобильной разработки ГК Юзтех, автор книги «Kotlin Multiplatform на практике»

Продолжаем наш рассказ про Swift Android SDK и его возможности. В предыдущей части мы рассмотрели, как устроена данная технология под капотом.

Пришло время посмотреть на реальной практике, а заодно и обсудить Swift SDK для Android. Кстати, к весне 2026 выпустили уже стабильный официальный релиз, работающий с Swift, начиная с версии 6.3.

Обратите внимание, несмотря на наличие официального релиза, данная технология не рекомендуется для прод-разработки. Также вы не напишете на ней приложение полностью.

Какие сценарии и слои сейчас поддерживаются:

  • бизнес-логика;

  • модели данных;

  • сетевой слой и хранение;

  • архитектурные компоненты;

  • фоновая обработка.

Та же история, что с KMP года 3 назад, но с оговорками.

UI рекомендуется реализовывать нативно, отдельно на Android и iOS. Также парадигма «write once, run everywhere» сейчас не работает. Только shared logic без кроссплатформенного UI-слоя.

Попробуем реализовать на Swift SDK для Android простое приложение новостей newsapi.org.

Наше приложение будет состоять из следующих модулей:

  • :app — наше Android-приложение, мы сделаем нативнуюNewsViewModel, а также UI на Compose;

  • :news-shared (Swift PM, таргет :newslib) — общий модуль, в котором будет лежать логика по работе с сетью, модели данных, бизнес-логика.

Исходники примера целиком: gitverse.ru/azharkova/swift-droid-news-sample.

Начнем с установки утилит и библиотек:

swiftly — менеджер тулчейнов Swift от самой Apple. Ставим нужную версию Swift (сейчас 6.4.1):

% swiftly install latest
Fetching the latest stable Swift release...
Installing Swift 6.4.1

Installing package in user home directory...
Swift 6.4.1 is installed successfully!
% swiftly use latest
The global default toolchain has been set to `Swift 6.4.1`

% swift --version
Apple Swift version 6.4.1 (swift-6.4.1-RELEASE)
Target: arm64-apple-macosx26.0

Android Swift SDK — сам SDK для кросс-компиляции под Android

% swift sdk install https://download.swift.org/swift-6.4.1-release/android-sdk/swift-6.4.1-RELEASE/swift-6.4.1-RELEASE_android.artifactbundle.tar.gz --checksum a356b79f8f1daa6ae3a66f4c556966ca20c0208b3659805cf30a83a0a31d6772

Android NDK: если NDK уже установлен, достаточно прописать переменную окружения ANDROID_NDK_HOME.

% curl -fSL -o ndk.zip https://dl.google.com/android/repository/android-ndk-r30-$(uname -s).zip
% unzip -qo ndk.zip
% export ANDROID_NDK_HOME=$PWD/android-ndk-r30

Готово.

Создаем обычное Android приложение и реализуем структуру подключения модуля.

Модуль news-shared отвечает за общий код. В нем у нас будет одновременно Gradle-модулем и описание Swift Package. В src/debug лежат сгенерированные библиотечные файлы jniLibs (готовые .so), в src/main/java находятся bridge-компоненты на Kotlin со связывающими конструкциями, в src/main/swift лежат Swift-исходники с Package.swift.

Пропишем структуру Package.Swift. Нам необходимо заявить наш модуль динамической библиотекой. А в качестве зависимостей прописать swift-java нужной версии. Добавить плагин JExtractSwiftPlugin из пакета swift-java для генерации Java-биндингов при сборке., прописать версию Swift, добавить флаги многопоточности. Обратите внимание, файл конфигурации мы исключаем из SPM.

Setup: Package.swift

let package = Package(
    name: "newslib",
    products: [
        .library(name: "newslib", type: .dynamic, targets: ["NewsShared"]),
    ],
    dependencies: [
        .package(url: "https://github.com/swiftlang/swift-java", from: "0.2.0"),
        .package(url: "https://github.com/swift-server/async-http-client.git", from: "1.33.1"),
    ],
    targets: [
        .target(
            name: "NewsShared",
            dependencies: [
                .product(
                    name: "AsyncHTTPClient",
                    package: "async-http-client",
                    condition: .when(platforms: [.android])
                ),
                .product(name: "SwiftJava", package: "swift-java"),
            ],
            exclude: ["swift-java.config"],
            swiftSettings: [
                .swiftLanguageMode(.v6),
                .unsafeFlags(["-Xfrontend", "-strict-concurrency=minimal"]),
            ],
            plugins: [
                .plugin(name: "JExtractSwiftPlugin", package: "swift-java"),
            ]
        ),
    ]
)

Также мы будем использовать AsyncHTTPClient для платформы Android. Это специальная библиотека для решений SwiftJava для работы с сетью.

Теперь займемся скриптом build.gradle.kts

Setup: build.gradle.kts

В Gradle-модуле news-shared указываем путь к скрипту сборки Swift-части и зависимость от SwiftKit runtime:

// news-shared/build.gradle.kts (фрагмент)
apply(from = "../swift-android.gradle.kts")

dependencies {
    implementation(libs.androidx.core.ktx)
    api(libs.swiftkit.core) // org.swift.swiftkit:swiftkit-core
}

swiftkit-core представляет собой runtime-библиотеку org.swift.swiftkit:swiftkit-core, в которой находятся SwiftArena, JNISwiftInstance и вся инфраструктура управления памятью.

Основной код по настройке приложения вы можете найти в репозитории с демо-проектом.

Setup: swift-java.config

Конфигурация генерации лежит в корне Swift-таргета и выглядит так:

{
  "javaPackage": "ru.azharkova.sampleapp.newslib.swift",
  "nativeLibraryName": "newslib",
  "mode": "jni",
  "asyncFuncMode": "legacyFuture"
}

Здесь важны все параметры: имя пакета для сгенерированных Java-классов, имя нативной библиотеки, режим jni (помним: FFM на Android не работает) и режим асинхронных функций legacyFuture включает мост CompletableFuture < - > async/await.

Приступим к реализации сетевого слоя. На iOS/macOS мы бы использовали URLSession, но FoundationNetworking на Android недоступен. Совместно с Android NDK мы можем использовать SwiftNIO. Для этого подключим библиотеку AsyncHTTPClient. Для запросов мы будем использовать конструкции специального сетевого транспорта:

private static func makeTransport() -> some ClientTransport {
    #if os(Android)
    return AsyncHTTPClientTransport(configuration: .init())
    #else
    return URLSessionTransport()
    #endif
}

#if os(Android) срабатывает для Android-таргета, #if canImport(Foundation) срабатывает для iOS/macOS. С помощью условной компиляции один и тот же Swift-код собирается и под Apple-платформы, и под Android.

Такую фабрику транспорта подключаем в обычный сетевой клиент, который пишем на Swift:

final class NetworkClient {
    private let transport: any NetworkTransport

    init(transport: any NetworkTransport = makeTransport())

    func execute<Request: NetworkRequest>(_ request: Request) async throws -> NetworkResponse
}

Но работать с сырыми данными будет неудобно, некрасиво. Нам потребуются дополнительные компоненты - различные декодеры и мапперы. Да и об архитектуре надо не забывать. Соединим это все в NewsApiService:

final class NewsApiService: NewsServicing {
    private let apiKey: String
    private let client: NetworkClient
    private let responseDecoder: NetworkResponseDecoder
    private let articleMapper: NewsArticleMapper
    private let errorMapper: NewsServiceErrorMapper

    func fetchTopHeadlines(country: String) async throws -> [NewsArticle] {
        //...
        do {
            let response = try await client.execute(TopHeadlinesRequest(...))
            let payload = try await responseDecoder.decode(
                NewsAPIResponse.self, from: response.data)

            return payload.articles.map(articleMapper.map)
        } catch {
            throw await errorMapper.map(error)
        }
    }
}

Выбор за тем, как будут называться компоненты в вашем коде, как всегда, за вами.

Теперь нам нужна публичная точка вызова нашего кода со стороны Java/ Android. Этот же компонент будет прокси для нашего кода. И вот его мы в экспортированном виде будем вызывать со стороны нативного приложения:

public final class NewsService {
    private let service: NewsApiService

    public init(apiKey: String) throws {
        self.service = try NewsApiService(apiKey: apiKey)
    }

    public func fetchTopHeadlines(country: String) async throws -> [NewsArticle] {
        try await service.fetchTopHeadlines(country: country)
    }
}

Сгенерированный jextract-класс NewsService виден из Kotlin как обычный класс. Вызывать и инициализовать мы его будем не напрямую в ViewModel, а сделаем еще один прокси (Bridge). Bridge на Kotlin хранит инстанс сгенерированного компонента из Swift-класса, а также вызов его метода через suspend:

class NewsServiceBridge(apiKey: String) {
    private val swiftNewsService: NewsService = NewsService.init(apiKey)

    suspend fun fetchTopHeadlines(country: String): List<NewsArticleUi> {
        val articles = swiftNewsService.fetchTopHeadlines(country).awaitSwift()
        return articles.map(::toUiArticle)
    }
}

Обратите внимание на .awaitSwift(): это наше Kotlin-расширение для CompletableFuture.

Подключим все в ViewModel. На стороне Android используем компонент из пакета Jetpack. В него мы добавим вызов сервиса через бридж:

class NewsViewModel(
    private val apiKey: String = BuildConfig.NEWS_API_KEY,
    private val country: String = BuildConfig.NEWS_COUNTRY
) : ViewModel() {
    private val newsServiceBridge: NewsServiceBridge?
    //...

    fun refresh() {

        viewModelScope.launch {
            _uiState.value = _uiState.value.copy(isLoading = true, errorMessage = null)

            val session = newsServiceBridge

            try {
                val articles = session.fetchTopHeadlines(country = country)
                _uiState.value = NewsUiState(articles = articles)
            } catch (throwable: Throwable) {...}
        }
    }
}

Однако, в такой реализации оно не будет работать. Даже не скомпилируется. Дело в том, что нам нужно прокинуть связку для управления памятью Swift в наш Kotlin-код. Как это сделать, посмотрим ниже.

Кроссплатформенные решения всегда требуют более внимательного отношения к управлению памятью, потому что кода генерируется больше, генерируются промежуточные стабы и прокси. Объекты должны удерживаться и освобождаться в Swift-куче, при этом оставаясь видимыми для Kotlin/Java GC. Это требует дополнительной работы и синхронизации двух менеджеров памяти. Также нужно больше внимания к переключению контекста как внутри платформы, так и между ними.

JVM управляет памятью через сборщик мусора (garbage collection), а Swift использует автоматический подсчёт ссылок (ARC). Эти две модели ничего не знают друг о друге. Java GC не может видеть Swift-типы и Swift-кучу: когда jextract передаёт Java-объекту Swift-инстанс, на самом деле передаётся opaque-указатель (адрес в памяти). По этому адресу объект находится в Swift-куче, и GC его не видит вообще.

Для решения этой проблемы используется механизм SwiftArena, в которой регистрируются адреса Swift-объектов, вызываемых из Java:

public interface SwiftArena {
    void register(SwiftInstance instance);

    static ClosableSwiftArena ofConfined() {
        return new ConfinedSwiftMemorySession();
    }

    static SwiftArena ofAuto() {
        ThreadFactory cleanerThreadFactory = r -> new Thread(r, "AutoSwiftArenaCleanerThread");
        return new AutoSwiftMemorySession(cleanerThreadFactory);
    }
}

Как вы видите, также используется привязка к определенным потокам.

Есть несколько видов специальных арен:

Интерфейс / класс

Ответственность

Жизненный цикл

ClosableSwiftArena

Расширяет SwiftArena и AutoCloseable — для использования с try-with-resources

Явный (close())

ConfinedSwiftMemorySession

ClosableSwiftArena, ведёт список ресурсов

Явный (close())

AutoSwiftMemorySession

Чистку запускает SwiftCleaner через GC

Неявный (GC-driven)

AllocatingSwiftArena

Реализация FFM: связывает время жизни Swift-объекта с MemorySegment

Явный или GC

Для корректной работы нам потребуется арена с явным механизмом закрытия и вызова очистки и арена с автоочисткой. Арена AutoSwiftMemorySession при регистрации инстанса объекта создает и хранит специальный метод очистки, ссылка на это действие будет позже передана компоненту SwiftCleaner:

final class AutoSwiftMemorySession implements SwiftArena {
    private final SwiftCleaner swiftCleaner;

    public AutoSwiftMemorySession(ThreadFactory cleanerThreadFactory) {
        this.swiftCleaner = SwiftCleaner.create(cleanerThreadFactory);
    }

    @Override
    public void register(SwiftInstance instance) {
        Objects.requireNonNull(instance, "value");

        // Гарантируем, что instance не захватится во время очистки,
        // поэтому можем игнорировать предупреждение ниже.
        var cleanupAction = instance.$createCleanup();
        swiftCleaner.register(instance, cleanupAction);
    }
}

Возвращаемся к нашему коду. Посмотрим на сгенерированный Java-код для нашего NewsService. Несмотря на то, что у нас этот параметр указан не был, инициализатор Swift типа явно требует SwiftArena на Java:

// Инстанс Swift
public final class NewsService {
    public init(apiKey: String) {}
}

// JNISwiftInstance
public static NewsService init(java.lang.String apiKey, SwiftArena swiftArena) throws Exception {
    return NewsService.wrapMemoryAddressUnsafe(NewsService.$init(apiKey), swiftArena);
}

private NewsService(long selfPointer, SwiftArena swiftArena) {
    SwiftObjects.requireNonZero(selfPointer, "selfPointer");
    this.selfPointer = selfPointer;

    // Only register once we have fully initalized the object
    // since this will need the object pointer.
    swiftArena.register(this);
}

public static NewsService wrapMemoryAddressUnsafe(long selfPointer, SwiftArena swiftArena) {
    return new NewsService(selfPointer, swiftArena);
}

public static NewsService wrapMemoryAddressUnsafe(long selfPointer) {
    return new NewsService(selfPointer, SwiftMemoryManagement.DEFAULT_SWIFT_JAVA_AUTO_ARENA);
}

Поэтому нам нужно будет его передать.

Арена регистрирует объект только после полной инициализации, поскольку ей нужен валидный указатель. Вызов метода у обёртки представляет собой обращение по указателю $memoryAddress() к настоящему Swift-инстансу:

Передаваемый инстанс арены должен быть проинициализирован явно: без настоящей привязки SwiftArena поиск объекта по указателю может привести к аварийному завершению.

Обратную сторону жизненного цикла представляет функция уничтожения. Сгенерированный $createDestroyFunction возвращает Runnable, который вызывает SwiftObjects.destroy по сохранённому указателю и адресу метаданных типа:

@Override
public Runnable $createDestroyFunction() {
    long self$ = this.$memoryAddress();
    long selfType$ = this.$typeMetadataAddress();
    if (CallTraces.TRACE_DOWNCALLS) {
        CallTraces.traceDowncall("NewsService.$createDestroyFunction",
            "this", this,
            "self", self$);
    }
    return new Runnable() {
        @Override
        public void run() {
            if (CallTraces.TRACE_DOWNCALLS) {
                CallTraces.traceDowncall("NewsService.$destroy", "self", self$);
            }
            SwiftObjects.destroy(self$, selfType$);
        }
    };
}

При этом объект остаётся видимым для GC. Wrapper в Java-куче собирается GC как обычно, а вот Swift-память освобождается только тогда, когда арена (или cleaner) вызовет destroy-функцию.

Хорошо, но мы забыли указать, как мы эту арену создаем, откуда берем и к чему она привязана. Конечно же, к жизненному циклу ViewModel. Создаём ClosableSwiftArena через SwiftArena.ofConfined(), передаём её в bridge, а в onCleared() закрываем и вызываем очистку:

class NewsViewModel(
    private val swiftArena: ClosableSwiftArena = SwiftArena.ofConfined()
) : ViewModel() {

    private val newsServiceBridge: NewsServiceBridge?

    init {
        var createdSession: NewsServiceBridge? = null
        try {
            createdSession = NewsServiceBridge(
                swiftArena = swiftArena
            )
        } catch (throwable: Throwable) {
            initializationError = throwable
        }

        newsServiceBridge = createdSession
    }

    override fun onCleared() {
        newsServiceBridge?.close()
        super.onCleared()
    }
}

А в bridge при этом реализует интерфейс AutoCloseable для корректного вызова очистки арены для финального освобождения ресурсов:

class NewsServiceBridge(
    private val swiftArena: ClosableSwiftArena
) : AutoCloseable {
    // Здесь хранится именно generated Swift instance, а не Kotlin service.
    private val swiftNewsService: NewsService = NewsService.init(swiftArena)

    suspend fun fetchTopHeadlines(country: String): List<NewsArticleUi> {
        val articles = swiftNewsService.fetchTopHeadlines(country, swiftArena).awaitSwift()
        return articles.map(::toUiArticle)
    }

    override fun close() {
        swiftArena.close()
    }
}

Итоговая цепочка: ViewModel уничтожается → onCleared() → bridge.close() → swiftArena.close() → destroy всех зарегистрированных Swift-объектов.

Управление памятью всегда связано с многопоточностью. JVM concurrency изначально несовместима со Swift concurrency. Но у нас есть решение. При этом мы можем выбрать, сделать все быстро и автоматически через конвертацию CompletableFuture <-> async/await или вручную через @_cdecl, подключение/отключение указателей к скоупу исполнения (как в графе для Kotlin/Native). Мы пойдем простым путем. Для поддержки CompletableFuture < - > async/await в конфигурационном файле добавим строку "asyncFuncMode": "legacyFuture". И на этом все. Сгенерированный метод будет выглядеть так:

public java.util.concurrent.Future<NewsArticle[]> fetchTopHeadlines(java.lang.String country,
SwiftArena swiftArena) {
    org.swift.swiftkit.core.SimpleCompletableFuture<long[]> future$ = new
org.swift.swiftkit.core.SimpleCompletableFuture<long[]>();
    NewsService.$fetchTopHeadlines(country, this.$memoryAddress(), future$);
    return future$.thenApply((futureResult$) -> {
      return Arrays.stream(futureResult$).mapToObj((pointer) -> {
        return NewsArticle.wrapMemoryAddressUnsafe(pointer, swiftArena);
      } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+JavaTranslation.swift:2024
      ).toArray(NewsArticle[]::new);
    } // render(_:_:) @ JExtractSwiftLib/JNISwift2JavaGenerator+JavaTranslation.swift:2024
    );
}

Swift async-функция автоматически маппится в Future, а каждый элемент результата (указатель) оборачивается в Java-обёртку через wrapMemoryAddressUnsafe с той же ареной. Для сахара и удобства можно добавить расширение в Kotlin для вызова Future внутри корутины и корректной конвертации между ними:

private suspend fun <T> Future<T>.awaitSwift(): T = withContext(Dispatchers.IO) {
    get()
}

Добавляем вызов в наш bridge:

suspend fun fetchTopHeadlines(country: String): List<NewsArticleUi> {
    val articles = swiftNewsService.fetchTopHeadlines(country, swiftArena).awaitSwift()
    return articles.map(::toUiArticle)
}

Собираем Swift-модуль кросс-компиляцией под Android (swift build --arch aarch64-unknown-linux-android24), на выходе получаем .so, передаём на устройство через adb push, не забыв вручную положить рядом libc++_shared.so. После Gradle-сборки в build/generated/jniLibs/arm64-v8a оказывается следующий набор библиотек:

Тут и libnewslib.so / libnewsshared.so (наша логика), и среда исполнения Swift: libswiftCore.so, libswift_Concurrency.so, libFoundation.so, libdispatch.so, libSwiftJava.so и десяток других. Сгенерированного кода очень много, и держать его под контролем версий смысла нет.

Интересный нюанс: папка build модуля news-shared весит 992 МБ. Сама .so в приложении заметно скромнее, APK вырос примерно на 8 МБ, но процесс сборки тяжёлый и долгий.

Получаем Android-приложение, в котором лента новостей загружается Swift-кодом через AsyncHTTPClient, маппится в UI-модели и показывается в Compose:

Репозиторий сэмпла: gitverse.ru/azharkova/swift-droid-news-sample.

Когда же будет хорошо (приемлемо) это использовать:

  • Just for fun — хобби-проекты, эксперименты, участие в развитии технологии;

  • Расширение существующего iOS-приложения на Android — когда бизнес-логика уже написана на Swift и команда хочет переиспользовать её без переписывания на Kotlin.

Для всего остального подойдут более зрелые инструменты.

Что было бы полезно получить в дальнейшем:

  • Полная поддержка async/await для Android (сейчас реализуется через обходное решение на CompletableFuture);

  • Кросс-компиляция и поддержка тестирования;

  • Расширения для VSCode.

Публичная дорожная карта лежит на GitHub: github.com/orgs/swiftlang/projects/17.

Источники

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.