Начало работы, шаг за шагом

18 апреля 2023 г.: В преддверии KotlinConf 2023 многоплатформенная поддержка Compose для iOS была повышена с экспериментальной до альфа-версии. Официальная документация значительно улучшена, но все, что я вам здесь показываю, остается в силе.

27 февраля 2023 г.: Спасибо Владимир Стельмащук за pull request.

Обновлено 13 февраля 2023 г.

Код:

https://github.com/marenovakovic/compose-ios

Я рекомендую использовать последнюю версию Android Studio.

Jetpack Compose, пожалуй, самое большое и лучшее, что случилось с разработкой Android с самого начала Android; это прямо там с принятием Kotlin — инженерный мрамор и абсолютная радость в использовании.

Благодаря усилиям Jetbrains по переносу Compose на мультиплатформу мы видим интересные способы создания приложений для Android и настольных компьютеров из единой базы кода. Но как насчет iOS? Оказывается, это тоже возможно… хотя не все так гладко. Сейчас я покажу вам, как это сделать, шаг за шагом.

Предпосылки

  • Это очевидно, но стоит отметить. Вы можете сделать это только из MacOS. Да, это Compose, но вам все еще нужны такие вещи, как симулятор.
  • Возможно, вы не сможете использовать все библиотеки, к которым вы привыкли из своей разработки для Android: компоненты архитектуры, библиотеки аккомпаниатора и т. д., потому что они не являются мультиплатформенными (пока?), но по большей части Compose будет работать так, как вы ожидаете. потому что поддержка iOS пока экспериментальная.

Первый подход

Вам нужен модуль для вашего приложения Compose для iOS. Это может быть отдельное приложение с приложением для iOS, являющимся единственным модулем в проекте (так же, как вы делаете это для нативной разработки под Android), или вы можете добавить еще один модуль в свой проект KMM.

Если вы хотите создать для этого новый проект, выберите Compose Multiplatform в IntelliJ IDEA или в новом мастере проектов AndroidStudio.

Внутри только что созданного модуля/проекта скопируйте и вставьте это суть в build.gradle.kts и используйте это суть для settings.gradle.kts.

Несколько вещей, чтобы объяснить о build.gradle.kts.

id("ord.jetbrains.compose") version "1.3.0-rc01, строка 7, и maven("https://maven.pkg.jetbrains.space/public/p/compose/dev"), строка 16 — это способ добавления Compose Multiplatform в приложение.

Мы определяем цели следующим образом:

iosX64("uikitX64") {
    binaries {
        executable {
            entryPoint = "main"
            freeCompilerArgs += listOf(
                "-linker-option", "-framework", "-linker-option", "Metal",
                "-linker-option", "-framework", "-linker-option", "CoreText",
                "-linker-option", "-framework", "-linker-option", "CoreGraphics"
            )
        }
    }
}
iosArm64("uikitArm64") {
    binaries {
        executable {
            entryPoint = "main"
            freeCompilerArgs += listOf(
                "-linker-option", "-framework", "-linker-option", "Metal",
                "-linker-option", "-framework", "-linker-option", "CoreText",
                "-linker-option", "-framework", "-linker-option", "CoreGraphics"
            )
            freeCompilerArgs += "-Xdisable-phases=VerifyBitcode"
        }
    }
}

Ничего особенного, верно?

Теперь давайте посмотрим на блок compose.experimental, который требует дополнительных пояснений. uikit.application позволяет настроить приложение для iOS. Вы видите такие вещи, как bundleIdPrefix и projectName. bundleIdPrefix похоже на applicationId для нас, разработчиков Android, а projectName говорит само за себя.

Теперь давайте перейдем к deployConfiguration. Вы развертываете/запускаете свое iOS-приложение с задачами Gradle; вы можете увидеть их в комментариях. Это iosDeployIPhone13ProDebug и iosDeployIPadDebug. Мы также определяем устройство, на котором мы хотим, чтобы эта задача запускала наше приложение. Имя, которое вы укажете внутри simulator("xxx"), будет определять вашу задачу Gradle для этой конфигурации, и оно будет называться xxx. Задача Gradle будет iosDeployxxxDebug.

Возможно, вам уже интересно, что такое teamId. Честно говоря, я не совсем уверен, но думаю, что это нужно для сборки релизной версии вашего приложения; отладка будет работать нормально без него. Для этого вам необходимо членство Apple Developer. Вы получите teamId оттуда.

Вот и все для настройки Gradle. Перейдем к файловой структуре. Внутри module/src вам нужно создать uikitMain/kotlin , а внутри него вам нужен файл main.uikit.kt. Это должно выглядеть так:

iosCompose — имя, которое я выбираю. Вы можете выбрать что-то другое.

Внутри main.uikit.kt вставьте следующий код:

Это минимальная настройка, и после того, как вы на нее посмотрите, она перестает пугать. На данный момент вам даже не нужно ничего об этом знать, за исключением того, что ваше приложение написано в строке 43 этого описания:

window!!.rootViewController = Application("Your Application") {
            Text("Helo World, from Compose iOS app!!!")
        }

Application — это функция, которая принимает функцию @Composable. Это ваша точка входа в мир Compose, и теперь вы можете использовать свои навыки Compose, как обычно.

Осталось только запустить приложение. Но не так быстро. Нам нужен еще один шаг. Вам нужно будет сделать это только в том случае, если вы никогда не запускали какое-либо приложение iOS на симуляторе из Xcode.

И это ваша задача на сегодня.

Создайте новое приложение iOS в Xcode и запустите приложение. Это необходимо, потому что вам нужен сертификат. Xcode предложит вам установить его, для чего потребуется пароль login.keychain.

И, наконец, откройте свой терминал и запустите ./gradlew iosDeployIPhone13ProDebug или найдите эту задачу в окне инструмента Gradle в Android Studio или IntellijIDEA.

Я должен отметить, что этот подход очень ограничен. Все, что вы хотите использовать, должно быть в одном модуле. Мне не удалось импортировать @Composables из модуля shared, и вы не можете обмениваться @Composables между платформами. наш модуль полностью автономен, и у нас нет проекта для приложения iOS.

Существует лучший способ…

Второй подход

Подход, который мы рассмотрим сейчас, намного лучше: мы сможем повторно использовать @Composable между платформами, и у нас будет доступ к нативному приложению iOS.

Для этого подхода у нас будут все наши @Composable в нашем модуле shared.

Для этого нам понадобится классическая мультиплатформенная установка Compose:

  1. settings.gradle.kts
pluginManagement {
    repositories {
        google()
        gradlePluginPortal()
        maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
        mavenCentral()
    }
}

2. Уровень проекта build.gradle.kts

plugins {
    //trick: for the same plugin versions in all sub-modules
    id("com.android.application").version("8.1.0-alpha04").apply(false)
    id("com.android.library").version("8.1.0-alpha04").apply(false)
    id("org.jetbrains.compose").version("1.3.0").apply(false)
    kotlin("android").version("1.8.0").apply(false)
    kotlin("multiplatform").version("1.8.0").apply(false)
    id("org.jetbrains.kotlin.jvm") version "1.8.0" apply false
}

allprojects {
    repositories {
        google()
        mavenCentral()
        maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
    }
}

3. shared модуль build.gradle.kts

plugins {
    kotlin("multiplatform")
    kotlin("native.cocoapods")
    id("com.android.library")
    id("org.jetbrains.compose")
}

kotlin {
    android()

    ios()
    iosSimulatorArm64()

    cocoapods {
        summary = "Some description for the Shared Module"
        homepage = "Link to the Shared Module homepage"
        version = "1.0"
        ios.deploymentTarget = "14.1"
        podfile = project.file("../ios/Podfile")
        framework {
            baseName = "shared"
            isStatic = true
        }
    }
    
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(compose.ui)
                implementation(compose.foundation)
                implementation(compose.material)
                implementation(compose.runtime)
            }
        }
        val androidMain by getting
        val iosMain by getting {
            dependsOn(commonMain)
        }
        val iosSimulatorArm64Main by getting {
            dependsOn(iosMain)
        }
    }
}

android {
    namespace = "com.example.compose_ios"
    compileSdk = 33
    defaultConfig {
        minSdk = 24
        targetSdk = 33
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSIONbuild.gradle.kts8
        targetCompatibility = JavaVersion.VERSIONbuild.gradle.kts8
    }
}

И с этим установка готова. Перейдем к реализации.

Главное здесь то, что наши @Composables внутри shared модуля должны быть внутренними.

внутренний означает, что он не будет виден за пределами модуля, в котором он определен, в нашем случае shared, так как же мы собираемся делиться @Composables? У нас будут файлы main.android.kt и main.ios.kt внутри shared/androidMain и shared/iosMain соответственно. Оттуда мы будем вызывать наши внутренние @Composables. Так:

  1. shared/commonMain. Это наше фактическое приложение, которым мы будем делиться на разных платформах.
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier

@Composable
internal fun ExampleApplication() {
    MaterialTheme {
        Scaffold(
            topBar = { TopAppBar { Text("Hello") } },
            content = {
                Box(
                    modifier = Modifier.fillMaxSize(),
                    contentAlignment = Alignment.Center,
                ) {
                    Text("From Compose!")
                }
            },
        )
    }
}

2. shared/androidMain, main.android.kt. Это будет видно из нашего приложения/проекта для Android.

import androidx.compose.runtime.Composable

@Composable
fun Application() {
    ExampleApplication()
}

3. shared/iosMain, main.ios.kt. Здесь определяются ViewController, которые будут видны из нашего приложения/проекта iOS.

import androidx.compose.ui.window.Application
import platform.UIKit.UIViewController

fun MainViewController(): UIViewController =
    Application("Example Application") {
        ExampleApplication()
    }

Как и в первом подходе, Application — это наш вход в мир Compose.

Настройка этого ViewController для приложения iOS:

//iosApp.swift

import SwiftUI
import shared

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        let mainViewController = Main_iosKt.MainViewController()
        window?.rootViewController = mainViewController
        window?.makeKeyAndVisible()
        return true
    }
}

4. Нажмите эту кнопку запуска. В этом случае мы не используем эту специальную задачу Gradle. Просто запустите созданную для нас конфигурацию ios при создании проекта.