From 280a52e585d0a18f43abc7dd6c159a91c3abd79b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 16:19:53 +0200 Subject: [PATCH 01/15] Add kotlin gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9b10be5e8..62c33aac5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ build .externalNativeBuild .idea/* /.idea/* +.kotlin \ No newline at end of file From 20c51888ce8f5e87a8858ffaf98141f2661ad1ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 16:20:27 +0200 Subject: [PATCH 02/15] Add KMP shared module --- build.gradle.kts | 3 + gradle/libs.versions.toml | 5 + kmp/kmp-shared/.gitignore | 1 + kmp/kmp-shared/build.gradle.kts | 100 ++++++++++++++++++ .../src/androidMain/AndroidManifest.xml | 4 + .../kmp/kmp_shared/Platform.android.kt | 3 + .../com/example/kmp/kmp_shared/Platform.kt | 3 + .../example/kmp/kmp_shared/Platform.ios.kt | 3 + settings.gradle.kts | 21 ++-- 9 files changed, 133 insertions(+), 10 deletions(-) create mode 100644 kmp/kmp-shared/.gitignore create mode 100644 kmp/kmp-shared/build.gradle.kts create mode 100644 kmp/kmp-shared/src/androidMain/AndroidManifest.xml create mode 100644 kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/kmp_shared/Platform.android.kt create mode 100644 kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/Platform.kt create mode 100644 kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/Platform.ios.kt diff --git a/build.gradle.kts b/build.gradle.kts index 6b4ec9ad0..899385a15 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -10,6 +10,9 @@ plugins { alias(libs.plugins.kotlin.parcelize) apply false alias(libs.plugins.compose.compiler) apply false alias(libs.plugins.kotlin.serialization) apply false + alias(libs.plugins.kotlin.multiplatform) apply false + alias(libs.plugins.android.kotlin.multiplatform.library) apply false + alias(libs.plugins.android.lint) apply false } apply("${project.rootDir}/buildscripts/toml-updater-config.gradle") diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 77f80c6d1..6e672c702 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -189,6 +189,8 @@ play-services-wearable = { module = "com.google.android.gms:play-services-wearab validator-push = { module = "com.google.android.wearable.watchface.validator:validator-push", version.ref = "validatorPush" } wear-compose-material = { module = "androidx.wear.compose:compose-material", version.ref = "wearComposeMaterial" } wear-compose-material3 = { module = "androidx.wear.compose:compose-material3", version.ref = "wearComposeMaterial3" } +jetbrains-kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", version.ref = "kotlin" } +kotlin-test = { group = "org.jetbrains.kotlin", name = "kotlin-test", version.ref = "kotlin" } [plugins] android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" } @@ -201,3 +203,6 @@ kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "androidGradlePlugin" } +android-lint = { id = "com.android.lint", version.ref = "androidGradlePlugin" } diff --git a/kmp/kmp-shared/.gitignore b/kmp/kmp-shared/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/kmp/kmp-shared/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/kmp/kmp-shared/build.gradle.kts b/kmp/kmp-shared/build.gradle.kts new file mode 100644 index 000000000..3cf323f14 --- /dev/null +++ b/kmp/kmp-shared/build.gradle.kts @@ -0,0 +1,100 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.android.kotlin.multiplatform.library) + alias(libs.plugins.android.lint) +} + +kotlin { + + // Target declarations - add or remove as needed below. These define + // which platforms this KMP module supports. + // See: https://kotlinlang.org/docs/multiplatform-discover-project.html#targets + androidLibrary { + namespace = "com.example.kmp.kmp_shared" + compileSdk = 36 + minSdk = 24 + + withHostTestBuilder { + } + + withDeviceTestBuilder { + sourceSetTreeName = "test" + }.configure { + instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + } + + // For iOS targets, this is also where you should + // configure native binary output. For more information, see: + // https://kotlinlang.org/docs/multiplatform-build-native-binaries.html#build-xcframeworks + + // A step-by-step guide on how to include this library in an XCode + // project can be found here: + // https://developer.android.com/kotlin/multiplatform/migrate + val xcfName = "kmp-sharedKit" + + iosX64 { + binaries.framework { + baseName = xcfName + } + } + + iosArm64 { + binaries.framework { + baseName = xcfName + } + } + + iosSimulatorArm64 { + binaries.framework { + baseName = xcfName + } + } + + // Source set declarations. + // Declaring a target automatically creates a source set with the same name. By default, the + // Kotlin Gradle Plugin creates additional source sets that depend on each other, since it is + // common to share sources between related targets. + // See: https://kotlinlang.org/docs/multiplatform-hierarchy.html + sourceSets { + commonMain { + dependencies { + implementation(libs.jetbrains.kotlin.stdlib) + // Add KMP dependencies here + } + } + + commonTest { + dependencies { + implementation(libs.kotlin.test) + } + } + + androidMain { + dependencies { + // Add Android-specific dependencies here. Note that this source set depends on + // commonMain by default and will correctly pull the Android artifacts of any KMP + // dependencies declared in commonMain. + } + } + + getByName("androidDeviceTest") { + dependencies { + implementation(libs.androidx.test.runner) + implementation(libs.androidx.test.core) + implementation(libs.androidx.test.ext.junit) + } + } + + iosMain { + dependencies { + // Add iOS-specific dependencies here. This a source set created by Kotlin Gradle + // Plugin (KGP) that each specific iOS target (e.g., iosX64) depends on as + // part of KMP’s default source set hierarchy. Note that this source set depends + // on common by default and will correctly pull the iOS artifacts of any + // KMP dependencies declared in commonMain. + } + } + } + +} \ No newline at end of file diff --git a/kmp/kmp-shared/src/androidMain/AndroidManifest.xml b/kmp/kmp-shared/src/androidMain/AndroidManifest.xml new file mode 100644 index 000000000..a5918e68a --- /dev/null +++ b/kmp/kmp-shared/src/androidMain/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/kmp_shared/Platform.android.kt b/kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/kmp_shared/Platform.android.kt new file mode 100644 index 000000000..1d27ef25c --- /dev/null +++ b/kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/kmp_shared/Platform.android.kt @@ -0,0 +1,3 @@ +package com.example.kmp.kmp_shared + +actual fun platform() = "Android" \ No newline at end of file diff --git a/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/Platform.kt b/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/Platform.kt new file mode 100644 index 000000000..3d9c545d5 --- /dev/null +++ b/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/Platform.kt @@ -0,0 +1,3 @@ +package com.example.kmp.kmp_shared + +expect fun platform(): String \ No newline at end of file diff --git a/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/Platform.ios.kt b/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/Platform.ios.kt new file mode 100644 index 000000000..5d877bae7 --- /dev/null +++ b/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/Platform.ios.kt @@ -0,0 +1,3 @@ +package com.example.kmp.kmp_shared + +actual fun platform() = "iOS" \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 316110b4e..7e9296053 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,4 @@ -val snapshotVersion : String? = System.getenv("COMPOSE_SNAPSHOT_ID") +val snapshotVersion: String? = System.getenv("COMPOSE_SNAPSHOT_ID") pluginManagement { repositories { @@ -30,13 +30,14 @@ dependencyResolutionManagement { rootProject.name = "snippets" include( ":bluetoothle", - ":compose:recomposehighlighter", - ":kotlin", - ":compose:snippets", - ":wear", - ":views", - ":misc", - ":identity:credentialmanager", - ":xr", - ":watchfacepush:validator" + ":compose:recomposehighlighter", + ":kotlin", + ":compose:snippets", + ":wear", + ":views", + ":misc", + ":identity:credentialmanager", + ":xr", + ":watchfacepush:validator", + ":kmp:kmp-shared", ) From a3162c0574f48635fdc15cf62534c5a8801a5e59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 17:27:21 +0200 Subject: [PATCH 03/15] Add xcode gitignore --- .gitignore | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 62c33aac5..b984a9c26 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,17 @@ build .externalNativeBuild .idea/* /.idea/* -.kotlin \ No newline at end of file +.kotlin + +### Xcode ### +## User settings +xcuserdata/ + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings From 49fc49a3c2e8d1adc6b907dd8094889338fd72cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 16:20:41 +0200 Subject: [PATCH 04/15] Add kmp ios app --- kmp/iosApp/Configuration/Config.xcconfig | 7 + kmp/iosApp/iosApp.xcodeproj/project.pbxproj | 379 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../iosApp/Assets.xcassets/Contents.json | 6 + kmp/iosApp/iosApp/ContentView.swift | 33 ++ kmp/iosApp/iosApp/Info.plist | 8 + .../Preview Assets.xcassets/Contents.json | 6 + kmp/iosApp/iosApp/iOSApp.swift | 10 + 10 files changed, 502 insertions(+) create mode 100644 kmp/iosApp/Configuration/Config.xcconfig create mode 100644 kmp/iosApp/iosApp.xcodeproj/project.pbxproj create mode 100644 kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 kmp/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 kmp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 kmp/iosApp/iosApp/Assets.xcassets/Contents.json create mode 100644 kmp/iosApp/iosApp/ContentView.swift create mode 100644 kmp/iosApp/iosApp/Info.plist create mode 100644 kmp/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 kmp/iosApp/iosApp/iOSApp.swift diff --git a/kmp/iosApp/Configuration/Config.xcconfig b/kmp/iosApp/Configuration/Config.xcconfig new file mode 100644 index 000000000..1ebca56e2 --- /dev/null +++ b/kmp/iosApp/Configuration/Config.xcconfig @@ -0,0 +1,7 @@ +TEAM_ID= + +PRODUCT_NAME=KotlinProject +PRODUCT_BUNDLE_IDENTIFIER=org.example.project.KotlinProject$(TEAM_ID) + +CURRENT_PROJECT_VERSION=1 +MARKETING_VERSION=1.0 \ No newline at end of file diff --git a/kmp/iosApp/iosApp.xcodeproj/project.pbxproj b/kmp/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 000000000..80071710c --- /dev/null +++ b/kmp/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,379 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + B9DA97B12DC1472C00A4DA20 /* KotlinProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KotlinProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + B99700CA2DC9B8D800C7335B /* Exceptions for "iosApp" folder in "iosApp" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = B9DA97B02DC1472C00A4DA20 /* iosApp */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + B9DA97B32DC1472C00A4DA20 /* iosApp */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + B99700CA2DC9B8D800C7335B /* Exceptions for "iosApp" folder in "iosApp" target */, + ); + path = iosApp; + sourceTree = ""; + }; + B9DA98002DC14AA900A4DA20 /* Configuration */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Configuration; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + B9DA97AE2DC1472C00A4DA20 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + B9DA97A82DC1472C00A4DA20 = { + isa = PBXGroup; + children = ( + B9DA98002DC14AA900A4DA20 /* Configuration */, + B9DA97B32DC1472C00A4DA20 /* iosApp */, + B9DA97B22DC1472C00A4DA20 /* Products */, + ); + sourceTree = ""; + }; + B9DA97B22DC1472C00A4DA20 /* Products */ = { + isa = PBXGroup; + children = ( + B9DA97B12DC1472C00A4DA20 /* KotlinProject.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + B9DA97B02DC1472C00A4DA20 /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = B9DA97BF2DC1472D00A4DA20 /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + B9DA97F42DC1497100A4DA20 /* Compile Kotlin Framework */, + B9DA97AD2DC1472C00A4DA20 /* Sources */, + B9DA97AE2DC1472C00A4DA20 /* Frameworks */, + B9DA97AF2DC1472C00A4DA20 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + B9DA97B32DC1472C00A4DA20 /* iosApp */, + ); + name = iosApp; + packageProductDependencies = ( + ); + productName = iosApp; + productReference = B9DA97B12DC1472C00A4DA20 /* KotlinProject.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + B9DA97A92DC1472C00A4DA20 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + B9DA97B02DC1472C00A4DA20 = { + CreatedOnToolsVersion = 16.2; + }; + }; + }; + buildConfigurationList = B9DA97AC2DC1472C00A4DA20 /* Build configuration list for PBXProject "iosApp" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = B9DA97A82DC1472C00A4DA20; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = B9DA97B22DC1472C00A4DA20 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + B9DA97B02DC1472C00A4DA20 /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B9DA97AF2DC1472C00A4DA20 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + B9DA97F42DC1497100A4DA20 /* Compile Kotlin Framework */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Compile Kotlin Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + B9DA97AD2DC1472C00A4DA20 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + B9DA97BD2DC1472D00A4DA20 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + B9DA97BE2DC1472D00A4DA20 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + B9DA97C02DC1472D00A4DA20 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = B9DA98002DC14AA900A4DA20 /* Configuration */; + baseConfigurationReferenceRelativePath = Config.xcconfig; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + B9DA97C12DC1472D00A4DA20 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = B9DA98002DC14AA900A4DA20 /* Configuration */; + baseConfigurationReferenceRelativePath = Config.xcconfig; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + B9DA97AC2DC1472C00A4DA20 /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B9DA97BD2DC1472D00A4DA20 /* Debug */, + B9DA97BE2DC1472D00A4DA20 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B9DA97BF2DC1472D00A4DA20 /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B9DA97C02DC1472D00A4DA20 /* Debug */, + B9DA97C12DC1472D00A4DA20 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = B9DA97A92DC1472C00A4DA20 /* Project object */; +} \ No newline at end of file diff --git a/kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/kmp/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/kmp/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/kmp/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/kmp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/kmp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..230588010 --- /dev/null +++ b/kmp/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/kmp/iosApp/iosApp/Assets.xcassets/Contents.json b/kmp/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/kmp/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/kmp/iosApp/iosApp/ContentView.swift b/kmp/iosApp/iosApp/ContentView.swift new file mode 100644 index 000000000..a206b8abb --- /dev/null +++ b/kmp/iosApp/iosApp/ContentView.swift @@ -0,0 +1,33 @@ +import SwiftUI +import Shared + +struct ContentView: View { + @State private var showContent = false + var body: some View { + VStack { + Button("Click me!") { + withAnimation { + showContent = !showContent + } + } + + if showContent { + VStack(spacing: 16) { + Image(systemName: "swift") + .font(.system(size: 200)) + .foregroundColor(.accentColor) + Text("SwiftUI: \(Greeting().greet())") + } + .transition(.move(edge: .top).combined(with: .opacity)) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) + .padding() + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/kmp/iosApp/iosApp/Info.plist b/kmp/iosApp/iosApp/Info.plist new file mode 100644 index 000000000..11845e1da --- /dev/null +++ b/kmp/iosApp/iosApp/Info.plist @@ -0,0 +1,8 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + + diff --git a/kmp/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/kmp/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/kmp/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/kmp/iosApp/iosApp/iOSApp.swift b/kmp/iosApp/iosApp/iOSApp.swift new file mode 100644 index 000000000..d83dca611 --- /dev/null +++ b/kmp/iosApp/iosApp/iOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct iOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} \ No newline at end of file From 04c433cb5ff9b9b9bf39823f36f2fcf3dc5e5d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 16:26:45 +0200 Subject: [PATCH 05/15] Add KMP ViewModel --- gradle/libs.versions.toml | 7 +++-- kmp/kmp-shared/build.gradle.kts | 26 ++++++---------- .../example/kmp/kmp_shared/MainViewModel.kt | 22 +++++++++++++ .../kmp/kmp_shared/ViewModelResolver.ios.kt | 31 +++++++++++++++++++ 4 files changed, 66 insertions(+), 20 deletions(-) create mode 100644 kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/MainViewModel.kt create mode 100644 kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/ViewModelResolver.ios.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 6e672c702..8bab1c791 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -130,6 +130,7 @@ androidx-lifecycle-runtime = { module = "androidx.lifecycle:lifecycle-runtime-kt androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle-runtime-compose" } androidx-lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycleService" } androidx-lifecycle-viewModelCompose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle-compose" } +androidx-lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "androidx-lifecycle-compose" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-compose" } androidx-material-icons-core = { module = "androidx.compose.material:material-icons-core" } androidx-media3-common = { module = "androidx.media3:media3-common", version.ref = "media3" } @@ -179,7 +180,7 @@ horologist-compose-layout = { module = "com.google.android.horologist:horologist horologist-compose-material = { module = "com.google.android.horologist:horologist-compose-material", version.ref = "horologist" } junit = { module = "junit:junit", version.ref = "junit" } kotlin-coroutines-okhttp = { module = "ru.gildor.coroutines:kotlin-coroutines-okhttp", version.ref = "kotlinCoroutinesOkhttp" } -kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" } +kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxCoroutinesGuava" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } @@ -199,10 +200,10 @@ compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = " gradle-versions = { id = "com.github.ben-manes.versions", version.ref = "gradle-versions" } hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } kotlin-android = "org.jetbrains.kotlin.android:2.2.10" +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "androidGradlePlugin" } kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } version-catalog-update = { id = "nl.littlerobots.version-catalog-update", version.ref = "version-catalog-update" } -kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } -android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "androidGradlePlugin" } android-lint = { id = "com.android.lint", version.ref = "androidGradlePlugin" } diff --git a/kmp/kmp-shared/build.gradle.kts b/kmp/kmp-shared/build.gradle.kts index 3cf323f14..f02283fa1 100644 --- a/kmp/kmp-shared/build.gradle.kts +++ b/kmp/kmp-shared/build.gradle.kts @@ -31,23 +31,15 @@ kotlin { // A step-by-step guide on how to include this library in an XCode // project can be found here: // https://developer.android.com/kotlin/multiplatform/migrate - val xcfName = "kmp-sharedKit" - iosX64 { - binaries.framework { - baseName = xcfName - } - } - - iosArm64 { - binaries.framework { - baseName = xcfName - } - } - - iosSimulatorArm64 { - binaries.framework { - baseName = xcfName + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + ).forEach { + it.binaries.framework { + export(libs.androidx.lifecycle.viewmodel) + baseName = "KmpKit" } } @@ -60,7 +52,7 @@ kotlin { commonMain { dependencies { implementation(libs.jetbrains.kotlin.stdlib) - // Add KMP dependencies here + api(libs.androidx.lifecycle.viewmodel) } } diff --git a/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/MainViewModel.kt b/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/MainViewModel.kt new file mode 100644 index 000000000..8eee933d5 --- /dev/null +++ b/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/MainViewModel.kt @@ -0,0 +1,22 @@ +package com.example.kmp.kmp_shared + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory + + +class MainViewModel( + private val repository: DataRepository, +) : ViewModel() { /* some logic */ } + +// ViewModelFactory that retrieves the data repository for your app. +val mainViewModelFactory = viewModelFactory { + initializer { + MainViewModel(repository = getDataRepository()) + } +} + +fun getDataRepository(): DataRepository = TODO() + + +class DataRepository \ No newline at end of file diff --git a/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/ViewModelResolver.ios.kt b/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/ViewModelResolver.ios.kt new file mode 100644 index 000000000..e13f2185e --- /dev/null +++ b/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/ViewModelResolver.ios.kt @@ -0,0 +1,31 @@ +package com.example.kmp.snipets + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.viewmodel.CreationExtras +import kotlinx.cinterop.BetaInteropApi +import kotlinx.cinterop.ObjCClass +import kotlinx.cinterop.getOriginalKotlinClass +import kotlin.reflect.KClass + +/** + * This function allows retrieving any ViewModel from Swift Code with generics. We only get + * [kotlinx.cinterop.ObjCClass] type for the [modelClass], because the interop between Kotlin and Swift code + * doesn't preserve the generic class, but we can retrieve the original KClass in Kotlin. + */ +@BetaInteropApi +@Throws(IllegalArgumentException::class) +fun ViewModelStore.resolveViewModel( + modelClass: ObjCClass, + factory: ViewModelProvider.Factory, + key: String?, + extras: CreationExtras? = null, +): ViewModel { + @Suppress("UNCHECKED_CAST") + val vmClass = getOriginalKotlinClass(modelClass) as? KClass + require(vmClass != null) { "The modelClass parameter must be a ViewModel type." } + + val provider = ViewModelProvider.Companion.create(this, factory, extras ?: CreationExtras.Empty) + return key?.let { provider[key, vmClass] } ?: provider[vmClass] +} \ No newline at end of file From 050302b200d79cf6be757fdc0888e52b0a1cc788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 16:29:26 +0200 Subject: [PATCH 06/15] Fix ios build --- kmp/iosApp/iosApp.xcodeproj/project.pbxproj | 4 ++-- .../mlykotom.xcuserdatad/xcschemes/xcschememanagement.plist | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/mlykotom.xcuserdatad/xcschemes/xcschememanagement.plist diff --git a/kmp/iosApp/iosApp.xcodeproj/project.pbxproj b/kmp/iosApp/iosApp.xcodeproj/project.pbxproj index 80071710c..d5b316933 100644 --- a/kmp/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/kmp/iosApp/iosApp.xcodeproj/project.pbxproj @@ -152,7 +152,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n"; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/../..\"\n./gradlew :kmp:kmp-shared:embedAndSignAppleFrameworkForXcode\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -376,4 +376,4 @@ /* End XCConfigurationList section */ }; rootObject = B9DA97A92DC1472C00A4DA20 /* Project object */; -} \ No newline at end of file +} diff --git a/kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/mlykotom.xcuserdatad/xcschemes/xcschememanagement.plist b/kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/mlykotom.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 000000000..ee3458dd7 --- /dev/null +++ b/kmp/iosApp/iosApp.xcodeproj/project.xcworkspace/xcuserdata/mlykotom.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,5 @@ + + + + + From 71c34428d6da65b29a8e83b60125e6b384f952ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 16:31:42 +0200 Subject: [PATCH 07/15] Add viewmodel snippet --- .../kotlin/com/example/kmp/kmp_shared/MainViewModel.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/MainViewModel.kt b/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/MainViewModel.kt index 8eee933d5..224685212 100644 --- a/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/MainViewModel.kt +++ b/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/MainViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory - +// [START android_kmp_viewmodel_class] class MainViewModel( private val repository: DataRepository, ) : ViewModel() { /* some logic */ } @@ -17,6 +17,7 @@ val mainViewModelFactory = viewModelFactory { } fun getDataRepository(): DataRepository = TODO() +// [END android_kmp_viewmodel_class] class DataRepository \ No newline at end of file From 71569c33ec1b5ab4b8e81ba6cc343e14bac08879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 16:51:05 +0200 Subject: [PATCH 08/15] Move to directory --- kmp/kmp-shared/build.gradle.kts | 2 +- .../kotlin/com/example/kmp/kmp_shared/Platform.android.kt | 3 --- .../kotlin/com/example/kmp/snippets/Platform.android.kt | 3 +++ .../commonMain/kotlin/com/example/kmp/kmp_shared/Platform.kt | 3 --- .../com/example/kmp/{kmp_shared => snippets}/MainViewModel.kt | 2 +- .../src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt | 3 +++ .../iosMain/kotlin/com/example/kmp/kmp_shared/Platform.ios.kt | 3 --- .../iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt | 3 +++ .../kmp/{kmp_shared => snippets}/ViewModelResolver.ios.kt | 0 9 files changed, 11 insertions(+), 11 deletions(-) delete mode 100644 kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/kmp_shared/Platform.android.kt create mode 100644 kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/snippets/Platform.android.kt delete mode 100644 kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/Platform.kt rename kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/{kmp_shared => snippets}/MainViewModel.kt (94%) create mode 100644 kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt delete mode 100644 kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/Platform.ios.kt create mode 100644 kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt rename kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/{kmp_shared => snippets}/ViewModelResolver.ios.kt (100%) diff --git a/kmp/kmp-shared/build.gradle.kts b/kmp/kmp-shared/build.gradle.kts index f02283fa1..ed832ccec 100644 --- a/kmp/kmp-shared/build.gradle.kts +++ b/kmp/kmp-shared/build.gradle.kts @@ -10,7 +10,7 @@ kotlin { // which platforms this KMP module supports. // See: https://kotlinlang.org/docs/multiplatform-discover-project.html#targets androidLibrary { - namespace = "com.example.kmp.kmp_shared" + namespace = "com.example.kmp.snippets" compileSdk = 36 minSdk = 24 diff --git a/kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/kmp_shared/Platform.android.kt b/kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/kmp_shared/Platform.android.kt deleted file mode 100644 index 1d27ef25c..000000000 --- a/kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/kmp_shared/Platform.android.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.example.kmp.kmp_shared - -actual fun platform() = "Android" \ No newline at end of file diff --git a/kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/snippets/Platform.android.kt b/kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/snippets/Platform.android.kt new file mode 100644 index 000000000..dfdbaafad --- /dev/null +++ b/kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/snippets/Platform.android.kt @@ -0,0 +1,3 @@ +package com.example.kmp.snippets + +actual fun platform() = "Android" \ No newline at end of file diff --git a/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/Platform.kt b/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/Platform.kt deleted file mode 100644 index 3d9c545d5..000000000 --- a/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/Platform.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.example.kmp.kmp_shared - -expect fun platform(): String \ No newline at end of file diff --git a/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/MainViewModel.kt b/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt similarity index 94% rename from kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/MainViewModel.kt rename to kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt index 224685212..55afd7169 100644 --- a/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/kmp_shared/MainViewModel.kt +++ b/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt @@ -1,4 +1,4 @@ -package com.example.kmp.kmp_shared +package com.example.kmp.snippets import androidx.lifecycle.ViewModel import androidx.lifecycle.viewmodel.initializer diff --git a/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt b/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt new file mode 100644 index 000000000..362cf18ba --- /dev/null +++ b/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt @@ -0,0 +1,3 @@ +package com.example.kmp.snippets + +expect fun platform(): String \ No newline at end of file diff --git a/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/Platform.ios.kt b/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/Platform.ios.kt deleted file mode 100644 index 5d877bae7..000000000 --- a/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/Platform.ios.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.example.kmp.kmp_shared - -actual fun platform() = "iOS" \ No newline at end of file diff --git a/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt b/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt new file mode 100644 index 000000000..43c876f88 --- /dev/null +++ b/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt @@ -0,0 +1,3 @@ +package com.example.kmp.snippets + +actual fun platform() = "iOS" \ No newline at end of file diff --git a/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/ViewModelResolver.ios.kt b/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt similarity index 100% rename from kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/kmp_shared/ViewModelResolver.ios.kt rename to kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt From 4b6206dacdf39e541980ff6c9c69002e464ffb5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 16:56:50 +0200 Subject: [PATCH 09/15] Move kmp to :shared --- kmp/iosApp/iosApp.xcodeproj/project.pbxproj | 2 +- .../xcschemes/iosApp.xcscheme | 66 +++++++++++++++++++ kmp/{kmp-shared => shared}/.gitignore | 0 kmp/{kmp-shared => shared}/build.gradle.kts | 0 .../src/androidMain/AndroidManifest.xml | 0 .../example/kmp/snippets/Platform.android.kt | 0 .../com/example/kmp/snippets/MainViewModel.kt | 0 .../com/example/kmp/snippets/Platform.kt | 0 .../com/example/kmp/snippets/Platform.ios.kt | 0 .../kmp/snippets/ViewModelResolver.ios.kt | 0 settings.gradle.kts | 2 +- 11 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 kmp/iosApp/iosApp.xcodeproj/xcuserdata/mlykotom.xcuserdatad/xcschemes/iosApp.xcscheme rename kmp/{kmp-shared => shared}/.gitignore (100%) rename kmp/{kmp-shared => shared}/build.gradle.kts (100%) rename kmp/{kmp-shared => shared}/src/androidMain/AndroidManifest.xml (100%) rename kmp/{kmp-shared => shared}/src/androidMain/kotlin/com/example/kmp/snippets/Platform.android.kt (100%) rename kmp/{kmp-shared => shared}/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt (100%) rename kmp/{kmp-shared => shared}/src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt (100%) rename kmp/{kmp-shared => shared}/src/iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt (100%) rename kmp/{kmp-shared => shared}/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt (100%) diff --git a/kmp/iosApp/iosApp.xcodeproj/project.pbxproj b/kmp/iosApp/iosApp.xcodeproj/project.pbxproj index d5b316933..c868dec41 100644 --- a/kmp/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/kmp/iosApp/iosApp.xcodeproj/project.pbxproj @@ -152,7 +152,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/../..\"\n./gradlew :kmp:kmp-shared:embedAndSignAppleFrameworkForXcode\n"; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/../..\"\n./gradlew :kmp:shared:embedAndSignAppleFrameworkForXcode\n"; }; /* End PBXShellScriptBuildPhase section */ diff --git a/kmp/iosApp/iosApp.xcodeproj/xcuserdata/mlykotom.xcuserdatad/xcschemes/iosApp.xcscheme b/kmp/iosApp/iosApp.xcodeproj/xcuserdata/mlykotom.xcuserdatad/xcschemes/iosApp.xcscheme new file mode 100644 index 000000000..0fe25fa51 --- /dev/null +++ b/kmp/iosApp/iosApp.xcodeproj/xcuserdata/mlykotom.xcuserdatad/xcschemes/iosApp.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/kmp/kmp-shared/.gitignore b/kmp/shared/.gitignore similarity index 100% rename from kmp/kmp-shared/.gitignore rename to kmp/shared/.gitignore diff --git a/kmp/kmp-shared/build.gradle.kts b/kmp/shared/build.gradle.kts similarity index 100% rename from kmp/kmp-shared/build.gradle.kts rename to kmp/shared/build.gradle.kts diff --git a/kmp/kmp-shared/src/androidMain/AndroidManifest.xml b/kmp/shared/src/androidMain/AndroidManifest.xml similarity index 100% rename from kmp/kmp-shared/src/androidMain/AndroidManifest.xml rename to kmp/shared/src/androidMain/AndroidManifest.xml diff --git a/kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/snippets/Platform.android.kt b/kmp/shared/src/androidMain/kotlin/com/example/kmp/snippets/Platform.android.kt similarity index 100% rename from kmp/kmp-shared/src/androidMain/kotlin/com/example/kmp/snippets/Platform.android.kt rename to kmp/shared/src/androidMain/kotlin/com/example/kmp/snippets/Platform.android.kt diff --git a/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt b/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt similarity index 100% rename from kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt rename to kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt diff --git a/kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt b/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt similarity index 100% rename from kmp/kmp-shared/src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt rename to kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt diff --git a/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt b/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt similarity index 100% rename from kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt rename to kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt diff --git a/kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt b/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt similarity index 100% rename from kmp/kmp-shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt rename to kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt diff --git a/settings.gradle.kts b/settings.gradle.kts index 7e9296053..111fd452e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -39,5 +39,5 @@ include( ":identity:credentialmanager", ":xr", ":watchfacepush:validator", - ":kmp:kmp-shared", + ":kmp:shared", ) From 0e15647f71188932ca1a409881a2fd2aac82cc21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 17:16:41 +0200 Subject: [PATCH 10/15] Add KMP regions --- kmp/iosApp/iosApp/ContentView.swift | 4 +-- .../iosApp/IosViewModelStoreOwner.swift | 33 +++++++++++++++++++ .../kmp/snippets/ViewModelResolver.ios.kt | 8 +++-- 3 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 kmp/iosApp/iosApp/IosViewModelStoreOwner.swift diff --git a/kmp/iosApp/iosApp/ContentView.swift b/kmp/iosApp/iosApp/ContentView.swift index a206b8abb..6afc99f02 100644 --- a/kmp/iosApp/iosApp/ContentView.swift +++ b/kmp/iosApp/iosApp/ContentView.swift @@ -1,5 +1,5 @@ import SwiftUI -import Shared +import KmpKit struct ContentView: View { @State private var showContent = false @@ -16,7 +16,7 @@ struct ContentView: View { Image(systemName: "swift") .font(.system(size: 200)) .foregroundColor(.accentColor) - Text("SwiftUI: \(Greeting().greet())") + Text("SwiftUI") } .transition(.move(edge: .top).combined(with: .opacity)) } diff --git a/kmp/iosApp/iosApp/IosViewModelStoreOwner.swift b/kmp/iosApp/iosApp/IosViewModelStoreOwner.swift new file mode 100644 index 000000000..9ecb40990 --- /dev/null +++ b/kmp/iosApp/iosApp/IosViewModelStoreOwner.swift @@ -0,0 +1,33 @@ +import Foundation +import KmpKit + +// [START android_kmp_viewmodel_ios_viewmodel_storeowner] +class IosViewModelStoreOwner: ObservableObject, ViewModelStoreOwner { + + let viewModelStore = ViewModelStore() + + /// This function allows retrieving the androidx ViewModel from the store. + /// It uses the utilify function to pass the generic type T to shared code + func viewModel( + key: String? = nil, + factory: ViewModelProviderFactory, + extras: CreationExtras? = nil + ) -> T { + do { + return try viewModelStore.resolveViewModel( + modelClass: T.self, + factory: factory, + key: key, + extras: extras + ) as! T + } catch { + fatalError("Failed to create ViewModel of type \(T.self)") + } + } + + /// This is called when this class is used as a `@StateObject` + deinit { + viewModelStore.clear() + } +} +// [END android_kmp_viewmodel_ios_viewmodel_storeowner] diff --git a/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt b/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt index e13f2185e..f2a28f52a 100644 --- a/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt +++ b/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt @@ -1,4 +1,4 @@ -package com.example.kmp.snipets +package com.example.kmp.snippets import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider @@ -9,9 +9,10 @@ import kotlinx.cinterop.ObjCClass import kotlinx.cinterop.getOriginalKotlinClass import kotlin.reflect.KClass +// [START android_kmp_viewmodel_resolve_viewmodel] /** * This function allows retrieving any ViewModel from Swift Code with generics. We only get - * [kotlinx.cinterop.ObjCClass] type for the [modelClass], because the interop between Kotlin and Swift code + * [ObjCClass] type for the [modelClass], because the interop between Kotlin and Swift code * doesn't preserve the generic class, but we can retrieve the original KClass in Kotlin. */ @BetaInteropApi @@ -28,4 +29,5 @@ fun ViewModelStore.resolveViewModel( val provider = ViewModelProvider.Companion.create(this, factory, extras ?: CreationExtras.Empty) return key?.let { provider[key, vmClass] } ?: provider[vmClass] -} \ No newline at end of file +} +// [END android_kmp_viewmodel_resolve_viewmodel] \ No newline at end of file From 503db88f94b97da79dc8dfc06c4241a7e32eb3e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 17:39:11 +0200 Subject: [PATCH 11/15] Add swift content --- kmp/iosApp/iosApp/ContentView.swift | 46 +++++++++---------- .../com/example/kmp/snippets/MainViewModel.kt | 2 +- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/kmp/iosApp/iosApp/ContentView.swift b/kmp/iosApp/iosApp/ContentView.swift index 6afc99f02..f8c992f92 100644 --- a/kmp/iosApp/iosApp/ContentView.swift +++ b/kmp/iosApp/iosApp/ContentView.swift @@ -1,33 +1,31 @@ -import SwiftUI +import Foundation import KmpKit +import SwiftUI +// [START android_kmp_viewmodel_ios_contentview] struct ContentView: View { - @State private var showContent = false - var body: some View { - VStack { - Button("Click me!") { - withAnimation { - showContent = !showContent - } - } - if showContent { - VStack(spacing: 16) { - Image(systemName: "swift") - .font(.system(size: 200)) - .foregroundColor(.accentColor) - Text("SwiftUI") - } - .transition(.move(edge: .top).combined(with: .opacity)) - } + /// Use the store owner as a StateObject to allow retrieving ViewModels and scoping it to this screen. + @StateObject private var viewModelStoreOwner = IosViewModelStoreOwner() + + var body: some View { + /// Retrieves the `MainViewModel` instance using the `viewModelStoreOwner`. + /// The `MainViewModel.Factory` and `creationExtras` are provided to enable dependency injection + /// and proper initialization of the ViewModel with its required `AppContainer`. + let mainViewModel: MainViewModel = viewModelStoreOwner.viewModel( + factory: MainViewModelKt.mainViewModelFactory + ) + // [START_EXCLUDE] + VStack(spacing: 16) { + Image(systemName: "swift") + .font(.system(size: 200)) + .foregroundColor(.accentColor) + Text("SwiftUI") } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top) .padding() + // [END_EXCLUDE] + // .. the rest of the SwiftUI code } } - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView() - } -} +// [END android_kmp_viewmodel_ios_contentview] diff --git a/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt b/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt index 55afd7169..611661690 100644 --- a/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt +++ b/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt @@ -16,7 +16,7 @@ val mainViewModelFactory = viewModelFactory { } } -fun getDataRepository(): DataRepository = TODO() +fun getDataRepository(): DataRepository = DataRepository() // [END android_kmp_viewmodel_class] From 437ede8830d667b381ff00434f378ff0423059f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 17:49:41 +0200 Subject: [PATCH 12/15] Add comments with file path --- kmp/iosApp/iosApp/ContentView.swift | 2 ++ kmp/iosApp/iosApp/IosViewModelStoreOwner.swift | 2 ++ .../kotlin/com/example/kmp/snippets/Platform.android.kt | 3 --- .../kotlin/com/example/kmp/snippets/MainViewModel.kt | 2 ++ .../commonMain/kotlin/com/example/kmp/snippets/Platform.kt | 3 --- .../iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt | 3 --- .../kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt | 4 +++- 7 files changed, 9 insertions(+), 10 deletions(-) delete mode 100644 kmp/shared/src/androidMain/kotlin/com/example/kmp/snippets/Platform.android.kt delete mode 100644 kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt delete mode 100644 kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt diff --git a/kmp/iosApp/iosApp/ContentView.swift b/kmp/iosApp/iosApp/ContentView.swift index f8c992f92..9e10c066d 100644 --- a/kmp/iosApp/iosApp/ContentView.swift +++ b/kmp/iosApp/iosApp/ContentView.swift @@ -3,6 +3,8 @@ import KmpKit import SwiftUI // [START android_kmp_viewmodel_ios_contentview] +// iosApp/ContentView.swift + struct ContentView: View { /// Use the store owner as a StateObject to allow retrieving ViewModels and scoping it to this screen. diff --git a/kmp/iosApp/iosApp/IosViewModelStoreOwner.swift b/kmp/iosApp/iosApp/IosViewModelStoreOwner.swift index 9ecb40990..b2e6c52df 100644 --- a/kmp/iosApp/iosApp/IosViewModelStoreOwner.swift +++ b/kmp/iosApp/iosApp/IosViewModelStoreOwner.swift @@ -2,6 +2,8 @@ import Foundation import KmpKit // [START android_kmp_viewmodel_ios_viewmodel_storeowner] +// iosApp/IosViewModelStoreOwner.swift + class IosViewModelStoreOwner: ObservableObject, ViewModelStoreOwner { let viewModelStore = ViewModelStore() diff --git a/kmp/shared/src/androidMain/kotlin/com/example/kmp/snippets/Platform.android.kt b/kmp/shared/src/androidMain/kotlin/com/example/kmp/snippets/Platform.android.kt deleted file mode 100644 index dfdbaafad..000000000 --- a/kmp/shared/src/androidMain/kotlin/com/example/kmp/snippets/Platform.android.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.example.kmp.snippets - -actual fun platform() = "Android" \ No newline at end of file diff --git a/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt b/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt index 611661690..526121d7c 100644 --- a/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt +++ b/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt @@ -5,6 +5,8 @@ import androidx.lifecycle.viewmodel.initializer import androidx.lifecycle.viewmodel.viewModelFactory // [START android_kmp_viewmodel_class] +// commonMain/MainViewModel.kt + class MainViewModel( private val repository: DataRepository, ) : ViewModel() { /* some logic */ } diff --git a/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt b/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt deleted file mode 100644 index 362cf18ba..000000000 --- a/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/Platform.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.example.kmp.snippets - -expect fun platform(): String \ No newline at end of file diff --git a/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt b/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt deleted file mode 100644 index 43c876f88..000000000 --- a/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/Platform.ios.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.example.kmp.snippets - -actual fun platform() = "iOS" \ No newline at end of file diff --git a/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt b/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt index f2a28f52a..1adb71226 100644 --- a/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt +++ b/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt @@ -10,6 +10,8 @@ import kotlinx.cinterop.getOriginalKotlinClass import kotlin.reflect.KClass // [START android_kmp_viewmodel_resolve_viewmodel] +// iosMain/ViewModelResolver.ios.kt + /** * This function allows retrieving any ViewModel from Swift Code with generics. We only get * [ObjCClass] type for the [modelClass], because the interop between Kotlin and Swift code @@ -30,4 +32,4 @@ fun ViewModelStore.resolveViewModel( val provider = ViewModelProvider.Companion.create(this, factory, extras ?: CreationExtras.Empty) return key?.let { provider[key, vmClass] } ?: provider[vmClass] } -// [END android_kmp_viewmodel_resolve_viewmodel] \ No newline at end of file +// [END android_kmp_viewmodel_resolve_viewmodel] From c8f077449b93699d27acf8128243c8e9bee9664f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 17:55:49 +0200 Subject: [PATCH 13/15] Add build-ios workflow --- .github/workflows/build-ios.yml | 51 +++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 .github/workflows/build-ios.yml diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml new file mode 100644 index 000000000..2d534e573 --- /dev/null +++ b/.github/workflows/build-ios.yml @@ -0,0 +1,51 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +name: Build snippets + +on: + push: + branches: [ '*' ] + paths: + - 'kmp/**' + - '.github/workflows/build-ios.yml' + pull_request: + branches: [ '*' ] + paths: + - 'kmp/**' + - '.github/workflows/build-ios.yml' + workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + build_ios: + name: Build iOS app + runs-on: macos-latest + steps: + - uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Checkout + uses: actions/checkout@v5 + + - name: Build iOS app + uses: mxcl/xcodebuild@v3 + with: + xcode: ^16 + scheme: iosApp + platform: iOS + action: build + working-directory: kmp/iosApp From 78fb291c4212ab3a49bb5658f81667eee2859ba8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Mlynari=C4=8D?= Date: Mon, 1 Sep 2025 17:59:15 +0200 Subject: [PATCH 14/15] Fix concurrency --- .github/workflows/build-ios.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml index 2d534e573..6e6d9a06a 100644 --- a/.github/workflows/build-ios.yml +++ b/.github/workflows/build-ios.yml @@ -26,7 +26,7 @@ on: - '.github/workflows/build-ios.yml' workflow_dispatch: concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.ref }}-build-ios cancel-in-progress: true # A workflow run is made up of one or more jobs that can run sequentially or in parallel jobs: From d5aa3a2369f15a9d357a23aed282ab858c0fa6d4 Mon Sep 17 00:00:00 2001 From: mlykotom <3973717+mlykotom@users.noreply.github.com> Date: Mon, 1 Sep 2025 16:05:59 +0000 Subject: [PATCH 15/15] Apply Spotless --- .../com/example/kmp/snippets/MainViewModel.kt | 19 +++++++++++++++++-- .../kmp/snippets/ViewModelResolver.ios.kt | 18 +++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt b/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt index 526121d7c..c4a06f5fd 100644 --- a/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt +++ b/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt @@ -1,3 +1,19 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.example.kmp.snippets import androidx.lifecycle.ViewModel @@ -21,5 +37,4 @@ val mainViewModelFactory = viewModelFactory { fun getDataRepository(): DataRepository = DataRepository() // [END android_kmp_viewmodel_class] - -class DataRepository \ No newline at end of file +class DataRepository diff --git a/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt b/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt index 1adb71226..1cf6fa2a3 100644 --- a/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt +++ b/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt @@ -1,13 +1,29 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package com.example.kmp.snippets import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStore import androidx.lifecycle.viewmodel.CreationExtras +import kotlin.reflect.KClass import kotlinx.cinterop.BetaInteropApi import kotlinx.cinterop.ObjCClass import kotlinx.cinterop.getOriginalKotlinClass -import kotlin.reflect.KClass // [START android_kmp_viewmodel_resolve_viewmodel] // iosMain/ViewModelResolver.ios.kt