Варианты настройки iosMain sourceSet’а в Kotlin Multiplatform Mobile

При использовании Kotlin Multiplatform Mobile сталкиваешься с непривычной особенностью — iOS код рассматривается компилятором в нескольких вариантах: iosArm64 и iosX64, а также iosArm32 (для поддержки девайсов вышедших до iPhone 5s). При разработке под iOS на Swift про эти особенности не думаешь, так как это скрыто в header’ах системных библиотек условиями препроцессора. 

Для разработчика чаще всего и не должно быть необходимости учитывать архитектуру процессора, на котором будет запущено приложение (особенно если архитектуры одинаковой битности, как iosArm64 и iosX64). И код под обе архитектуры полностью одинаковый, поэтому проект настраивают под использование одного источника исходного кода — iosMain. Есть несколько вариантов объединения ios кода в одном sourceSet, каждый со своими плюсами и минусами.

Commonizer в Kotlin 1.4

Kotlin Multiplatform позволяет строить иерархию из KotlinSourceSet’ов. Например, сделать промежуточный sourceSet со всем ios кодом, как на схеме ниже.

iosMain в иерархии (source — https://kotlinlang.org/docs/reference/mpp-share-on-platforms.html)

С такой настройкой можно расположить весь код связанный с ios в iosMain sourceSet. Он будет успешно компилироваться, но до Kotlin 1.4 IDE не могла корректно анализировать данный код, так как не известно под какую платформу нужно делать анализ — Arm64 или же X64. В результате мы получали ошибки в IDE (но для компилятора все было валидно):

С Kotlin 1.4 проблема поддержки IDE решена за счет нового инструмента — Commonizer. Он автоматически проводит поиск общего между iosArm64Main и iosX64Main и генерирует специальную iosMain klib, в которой содержатся все общие декларации, а IDE проводит анализ используя эту klib. Подробнее про commonizer вы можете узнать в выступлении разработчика Kotlin/Native.

Для настройки своего проекта под этот вариант нужно указать в build.gradle.kts:

plugins {
    kotlin("multiplatform")
}

kotlin {
    ios {
        binaries {
            framework {
                baseName = "shared"
            }
        }
    }
    sourceSets {
        val commonMain by getting
        val iosMain by getting
    }
}

А для включения commonizer добавляем в gradle.properties:

kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlin.native.enableDependencyPropagation=false

В результате получаем одно место с исходным кодом iOS и работающую помощь от IDE.

Но есть и ограничения — не всё iOS API доступно в iosMain. Например, протокол UITextFieldDelegateProtocol полностью пуст:

public expect interface UITextFieldDelegateProtocol : platform.darwin.NSObjectProtocol {
}

Хотя при работе из iosX64Main/iosArm64Main мы видим полный интерфейс:

public interface UITextFieldDelegateProtocol : platform.darwin.NSObjectProtocol {
    public open fun textField(textField: platform.UIKit.UITextField, shouldChangeCharactersInRange: kotlinx.cinterop.CValue<platform.Foundation.NSRange>, replacementString: kotlin.String): kotlin.Boolean

    public open fun textFieldDidBeginEditing(textField: platform.UIKit.UITextField): kotlin.Unit

    ...
}

А так-же при настройке cinterop (например при подключении cocoapods в Kotlin) все декларации не доступны в iosMain при просмотре через IDE (хотя для компилятора все будет корректно работать).

Настроенный пример можно посмотреть на GitHub.

Плюсы:

  1. промежуточный sourceSet полноценно поддерживается IDE

  2. отдельные gradle-задачи для компиляции обеих архитектур

Минусы:

  1. cInterop не видны для IDE в промежуточном sourceSet

  2. коммонизация работает только на 1 уровне иерархии (если за iosMain сделать appleMain для ios, macos – не будет работать обобщение)

  3. не все API доступно в промежуточном sourceSet

  4. внешние библиотеки должны иметь свой опубликованный промежуточный sourceSet (не важно как он зовется — важно какие таргеты в нем объединены)

Один sourceSet для iOS

Следующий подход указан в документации Kotlin Multiplatform Mobile. В данном случае предлагается на этапе конфигурирования gradle выбирать какой таргет нам использовать — iosX64 или iosArm64. И выбор этот делается на основе переменной окружения SDKNAME — она подставляется Xcode автоматически. Поэтому с данным подходом мы сможем скомпилировать под девайс только из Xcode.

Настройка делается следующим образом:

import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget

plugins {
    kotlin("multiplatform")
}

kotlin {
    val iosTarget: (String, KotlinNativeTarget.() -> Unit) -> KotlinNativeTarget =
        if (System.getenv("SDK_NAME")?.startsWith("iphoneos") == true)
            ::iosArm64
        else
            ::iosX64

    iosTarget("ios") {
        binaries {
            framework {
                baseName = "shared"
            }
        }
    }
    sourceSets {
        val commonMain by getting
        val iosMain by getting
    }
}

В итоге получаем iosMain полностью работающий и с IDE и с cInterop:

Настроенный пример можно посмотреть на GitHub.

Плюсы:

  1. iosMain содержит весь код под обе платформы

  2. cInterop корректно работает

Минусы:

  1. Конфигурация в gradle зависит от переменных окружения

  2. В gradle доступна только одна задача компиляции iOS, а какая архитектура будет собираться решается переменной окружения

  3. Для компиляции под девайс нужно собирать из Xcode

Arm64 sourceSet depends on X64

Выставление зависимостей между sourceSet можно использовать и не только для иерархии. Например указать зависимость iosArm64Main от iosX64Main.

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

plugins {
    kotlin("multiplatform")
}

kotlin {
    val ios = listOf(iosX64(), iosArm64())
    configure(ios) {
        binaries {
            framework {
                baseName = "shared"
            }
        }
    }
    sourceSets {
        val commonMain by getting
        val iosX64Main by getting
        val iosArm64Main by getting {
            dependsOn(iosX64Main)
        }
    }
}

А весь код в таком случае располагается в директории iosX64Main:

Настроенный пример можно посмотреть на GitHub.

Плюсы:

  1. код не дублирован, лежит в одном из sourceSet

  2. всё платформенное API доступно

  3. отдельные gradle-задачи для компиляции обеих архитектур

  4. cInterop корректно поддерживается

Минусы:

  1. до Kotlin 1.4 cInterop с такой конфигурацией не поддерживался (была ошибка о подключении некорректной архитектуры в линковку)

symlink Arm64 to X64

Последний вариант, используемый нами в IceRock, позволяет не дублировать код, использовать все API и cInterop, а также не требует сложных настроек. Чтобы не дублировать код мы просто создаем symlink для одного из ios sourceSet:

ln -s iosX64Main iosArm64Main

А в gradle настраиваем проект с двумя ios таргетами:

plugins {
    kotlin("multiplatform")
}

kotlin {
    val ios = listOf(iosX64(), iosArm64())
    configure(ios) {
        binaries {
            framework {
                baseName = "shared"
            }
        }
    }
    sourceSets {
        val commonMain by getting
    }
}

В результате получаем желаемый результат:

Настроенный пример можно посмотреть на GitHub.

Плюсы:

  1. код не дублирован, лежит в одном sourceSet, а symlink его отражает

  2. всё платформенное API доступно

  3. cinterop доступен и корректно работает на всех версиях Kotlin

Минусы:

  1. git изменения не видны при просмотре через symlink директорию

  2. IDE не замечает автоматически изменения symlink файлов (нужно делать reload directory или же просто работать всегда в одном сорссете)

  3. не работает на Windows (но для iOS и не нужно)

Let’s block ads! (Why?)

Read More

Recent Posts

Apple возобновила переговоры с OpenAI и Google для интеграции ИИ в iPhone

Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…

4 дня ago

Российская «дочка» Google подготовила 23 иска к крупнейшим игрокам рекламного рынка

Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…

5 дней ago

Google завершил обновление основного алгоритма March 2024 Core Update

Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…

5 дней ago

Нейросети будут писать тексты объявления за продавцов на Авито

У частных продавцов на Авито появилась возможность составлять текст объявлений с помощью нейросети. Новый функционал доступен в категории «Обувь, одежда,…

5 дней ago

Объявлены победители международной премии Workspace Digital Awards-2024

24 апреля 2024 года в Москве состоялась церемония вручения наград международного конкурса Workspace Digital Awards. В этом году участниками стали…

5 дней ago

Яндекс проведет гик-фестиваль Young Con

27 июня Яндекс проведет гик-фестиваль Young Con для студентов и молодых специалистов, которые интересуются технологиями и хотят работать в IT.…

6 дней ago