diff --git a/.github/workflows/build-ios.yml b/.github/workflows/build-ios.yml new file mode 100644 index 000000000..6e6d9a06a --- /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 }}-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: + 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 diff --git a/.gitignore b/.gitignore index 9b10be5e8..b984a9c26 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,17 @@ build .externalNativeBuild .idea/* /.idea/* +.kotlin + +### Xcode ### +## User settings +xcuserdata/ + +### Xcode Patch ### +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings 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..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" } @@ -189,6 +190,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" } @@ -197,7 +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" } +android-lint = { id = "com.android.lint", version.ref = "androidGradlePlugin" } 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..c868dec41 --- /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 :kmp: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 */; +} 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.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 @@ + + + + + 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/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..9e10c066d --- /dev/null +++ b/kmp/iosApp/iosApp/ContentView.swift @@ -0,0 +1,33 @@ +import Foundation +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. + @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 + } +} +// [END android_kmp_viewmodel_ios_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/IosViewModelStoreOwner.swift b/kmp/iosApp/iosApp/IosViewModelStoreOwner.swift new file mode 100644 index 000000000..b2e6c52df --- /dev/null +++ b/kmp/iosApp/iosApp/IosViewModelStoreOwner.swift @@ -0,0 +1,35 @@ +import Foundation +import KmpKit + +// [START android_kmp_viewmodel_ios_viewmodel_storeowner] +// iosApp/IosViewModelStoreOwner.swift + +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/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 diff --git a/kmp/shared/.gitignore b/kmp/shared/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/kmp/shared/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/kmp/shared/build.gradle.kts b/kmp/shared/build.gradle.kts new file mode 100644 index 000000000..ed832ccec --- /dev/null +++ b/kmp/shared/build.gradle.kts @@ -0,0 +1,92 @@ +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.snippets" + 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 + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64(), + ).forEach { + it.binaries.framework { + export(libs.androidx.lifecycle.viewmodel) + baseName = "KmpKit" + } + } + + // 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) + api(libs.androidx.lifecycle.viewmodel) + } + } + + 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/shared/src/androidMain/AndroidManifest.xml b/kmp/shared/src/androidMain/AndroidManifest.xml new file mode 100644 index 000000000..a5918e68a --- /dev/null +++ b/kmp/shared/src/androidMain/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ 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 new file mode 100644 index 000000000..c4a06f5fd --- /dev/null +++ b/kmp/shared/src/commonMain/kotlin/com/example/kmp/snippets/MainViewModel.kt @@ -0,0 +1,40 @@ +/* + * 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.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory + +// [START android_kmp_viewmodel_class] +// commonMain/MainViewModel.kt + +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 = DataRepository() +// [END android_kmp_viewmodel_class] + +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 new file mode 100644 index 000000000..1cf6fa2a3 --- /dev/null +++ b/kmp/shared/src/iosMain/kotlin/com/example/kmp/snippets/ViewModelResolver.ios.kt @@ -0,0 +1,51 @@ +/* + * 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 + +// [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 + * 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] +} +// [END android_kmp_viewmodel_resolve_viewmodel] diff --git a/settings.gradle.kts b/settings.gradle.kts index 316110b4e..111fd452e 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:shared", )