Irbisa · Шпаргалка

Interop KMP и Swift

ObjC-мост, suspend и Flow в Swift, SKIE, XCFramework, SPM, линковка, Swift export

Senior-разработчик · 15 вопросов

  1. 01

    Как типы Kotlin выглядят в Swift через Objective-C-мост: числа, nullable-примитивы, коллекции, nullability?

    Каждый тип Kotlin сначала отображается в свой аналог в Objective-C и только потом импортируется в Swift, поэтому non-null примитивы и String проходят чисто, а nullable-примитивы и коллекции чисел приходят упакованными.

    class FeedApi {  fun page(size: Int, cursor: Long?): List<Post> = TODO()  fun unreadCount(): Int? = null  fun ratings(): List<Int> = listOf(5, 4)}…
  2. 02

    Что происходит с дженериками Kotlin при переходе через Objective-C-мост и почему общий интерфейс Result<T> доходит до Swift как Any?

    В Objective-C есть только облегчённые дженерики (lightweight generics) у классов, поэтому параметр типа у Kotlin-класса сохраняется как подсказка для компилятора, а дженерик-интерфейсы, дженерик-функции и kotlin.Result его теряют, и Swift видит Any.

  3. 03

    Как iOS-команда вызывает из Swift каждое из этих Kotlin-объявлений?

    // Greeting.kt in the shared frameworkobject Analytics {  fun track(event: String, params: Map<String, String> = emptyMap()) {}} data class User(val id: Long, val name: String) class Loader {  fun load(id: Long): User = TODO()  fun load(id: String): User = TODO()} fun greet(user: User): String = "Hi, ${user.name}"

    Четыре вещи выглядят не по-свифтовски: аргумент по умолчанию пропадает, copy() превращается в doCopy(), вторая перегрузка получает подчёркивание, а функция верхнего уровня висит на классе GreetingKt, потому что в Objective-C нет аргументов по умолчанию, перегрузки и свободных функций.

  4. 04

    Почему switch в Swift по sealed-классу Kotlin требует ветку default и как вернуть исчерпывающую проверку?

    Заголовок фреймворка написан на Objective-C, а там нет закрытых иерархий: sealed-тип приходит как обычный класс или протокол с подклассами, поэтому Swift не может знать, что список случаев полный, и требует default.

    sealed interface AuthState {  data object Anonymous : AuthState  data class LoggedIn(val token: String) : AuthState  data class Failed(val reason: String) : AuthState}…
  5. 05

    Как Kotlin suspend fun выглядит из Swift и что происходит, когда Swift отменяет Task, который её ждёт?

    Она экспортируется как метод Objective-C с completion handler, который Swift импортирует как async throws.

    class Subscription(private val job: Job) {  fun cancel() = job.cancel()} class UserFacade(private val api: UserApi) {  private val scope = MainScope()…
  6. 06

    Найдите баг: когда сервер отвечает 404, iOS-приложение падает вместо того, чтобы показать ошибку из блока catch в Swift.

    class ApiException(val code: Int) : Exception("HTTP $code") class ProfileRepository(private val client: HttpClient) {    // Swift: do { try await repo.loadProfile(id: 42) } catch { show(error) }    suspend fun loadProfile(id: Long): Profile {        val response = client.get("profiles/$id")        if (!response.status.isSuccess()) {            throw ApiException(response.status.value)        }        return response.body()    }}

    У loadProfile нет @Throws, поэтому ApiException так и не превращается в NSError: рантайм Kotlin/Native считает его необработанным и завершает приложение, а catch в Swift не выполняется.

  7. 07

    Как потреблять Kotlin Flow из Swift и что дают KMP-NativeCoroutines и SKIE по сравнению с самописной обёрткой?

    Сырой Flow доходит до Swift как непрозрачный протокол с единственным методом collect(collector:completionHandler:), поэтому вы либо сами оборачиваете его в API «колбэк плюс хэндл», либо поручаете KMP-NativeCoroutines или SKIE превратить его в AsyncSequence.

    class Subscription(private val job: Job) {  fun cancel() = job.cancel()} class PostFacade(private val repo: PostRepo) {  private val scope = MainScope()…
  8. 08

    Что на самом деле генерирует SKIE и чего стоит его подключение для сборки?

    SKIE представляет собой плагин компилятора от Touchlab, который генерирует слой Swift поверх Objective-C-фреймворка и кладёт его в тот же фреймворк, так что Swift получает enum, async и AsyncSequence без установки чего-либо на стороне iOS.

  9. 09

    Как в Swift 6 со strict concurrency держать общий Kotlin-ViewModel в SwiftUI и обновлять UI только на главном потоке?

    Владейте Kotlin-объектом из одной @MainActor-модели, заставьте Kotlin вызывать все колбэки на главном потоке и никогда не передавайте экспортированные Kotlin-объекты между акторами: это классы Objective-C, и они не Sendable.

    import SwiftUIimport Shared @MainActor@Observablefinal class FeedModel {…
  10. 10

    Эта модель собирает общий Kotlin StateFlow через SKIE. Почему ни она, ни Kotlin-ViewModel никогда не освобождаются?

    @MainActorfinal class FeedModel: ObservableObject {  @Published var posts: [Post] = []  private let vm = FeedViewModel()   // Kotlin, exported via SKIE   init() {    Task { [weak self] in      guard let self else { return }      for await list in self.vm.posts {        self.posts = list      }    }  }}

    guard let self превращает слабый захват в сильный на всё время цикла, а цикл не заканчивается, потому что StateFlow никогда не завершается.

  11. 11

    Как спроектировать публичный API общего модуля, чтобы SwiftUI-код поверх него читался как Swift?

    Относитесь к экспортируемой поверхности как к спроектированному API с одним потребителем: всё public в общем модуле попадает в заголовок, поэтому экспортируйте только фасады уровня экрана с простыми конкретными типами.

    class Subscription(private val job: Job) {  fun cancel() = job.cancel()} object SharedFactory {  fun feedViewModel(): FeedViewModel = FeedViewModel(provideFeedRepo())   // internal constructor stays hidden…
  12. 12

    Как общий фреймворк попадает к iOS-команде со своим репозиторием и без JDK и чего стоит каждый способ доставки?

    Либо iOS-сборка сама компилирует Kotlin через Gradle, либо она потребляет версионированный XCFramework через Swift Package Manager.

    kotlin {  val xcf = XCFramework("Shared")            // one artifact, every iOS slice  listOf(iosArm64(), iosSimulatorArm64()).forEach { target ->    target.binaries.framework {      baseName = "Shared"      isStatic = true…
  13. 13

    Статический или динамический фреймворк для общего модуля: что меняется для времени запуска, размера приложения, расширений и символов крэшей?

    Статический фреймворк линкуется в каждый бинарник, который его использует; динамический собирается в отдельный образ, который dyld грузит при запуске.

  14. 14

    Брейкпоинт в .kt-файле не срабатывает из Xcode, а значения Kotlin видны как непрозрачные объекты. Что нужно, чтобы отлаживать через границу?

    LLDB умеет останавливаться внутри Kotlin, потому что компилятор Kotlin/Native генерирует отладочную информацию DWARF, но только в debug-фреймворке, чьи записанные пути к исходникам существуют на вашей машине; для читаемых значений вдобавок нужны LLDB-форматтеры xcode-kotlin.

    # Xcode > Build Phases > Run Script: the variant follows CONFIGURATIONcd "$SRCROOT/.."./gradlew :shared:embedAndSignAppleFrameworkForXcode# Exporting KOTLIN_FRAMEWORK_BUILD_TYPE=release in a Debug scheme# is one common reason breakpoints stop working.…
  15. 15

    Что меняет Swift export в Kotlin по сравнению с Objective-C-мостом и выпустили бы вы на нём релиз сегодня?

    Swift export заставляет компилятор Kotlin генерировать Swift-биндинги напрямую, так что API больше не обязан сначала укладываться в Objective-C.

    // shared/build.gradle.ktskotlin {  iosArm64()  iosSimulatorArm64()   swiftExport {…