From a23a7448ecf0e51e8264aab38a8fda27ba3347df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Thu, 4 Jun 2026 09:07:34 +0200 Subject: [PATCH 1/4] feat: introduce experimental KMP maps module wrapping Google Maps on Android and MapKit on iOS --- maps-compose-multiplatform/build.gradle.kts | 55 +++++++++++++++++++ .../compose/multiplatform/GoogleMap.kt | 40 ++++++++++++++ .../compose/multiplatform/GoogleMap.kt | 32 +++++++++++ .../compose/multiplatform/GoogleMap.kt | 51 +++++++++++++++++ settings.gradle.kts | 1 + 5 files changed, 179 insertions(+) create mode 100644 maps-compose-multiplatform/build.gradle.kts create mode 100644 maps-compose-multiplatform/src/androidMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt create mode 100644 maps-compose-multiplatform/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt create mode 100644 maps-compose-multiplatform/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt diff --git a/maps-compose-multiplatform/build.gradle.kts b/maps-compose-multiplatform/build.gradle.kts new file mode 100644 index 00000000..9eb0110d --- /dev/null +++ b/maps-compose-multiplatform/build.gradle.kts @@ -0,0 +1,55 @@ +/* + * Copyright 2026 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. + */ + +plugins { + kotlin("multiplatform") + id("com.android.kotlin.multiplatform.library") + alias(libs.plugins.compose.compiler) +} + +kotlin { + androidLibrary { + namespace = "com.google.maps.android.compose.multiplatform" + compileSdk = libs.versions.androidCompileSdk.get().toInt() + minSdk = libs.versions.androidMinSdk.get().toInt() + } + + // Enable iOS targets + iosX64() + iosArm64() + iosSimulatorArm64() + + sourceSets { + commonMain { + dependencies { + implementation("org.jetbrains.compose.runtime:runtime:1.7.3") + implementation("org.jetbrains.compose.foundation:foundation:1.7.3") + implementation("org.jetbrains.compose.ui:ui:1.7.3") + } + } + androidMain { + dependencies { + // Link the existing maps-compose module locally + api(project(":maps-compose")) + } + } + iosMain { + dependencies { + // Uses native MapKit via platform libraries + } + } + } +} diff --git a/maps-compose-multiplatform/src/androidMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt b/maps-compose-multiplatform/src/androidMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt new file mode 100644 index 00000000..d2696474 --- /dev/null +++ b/maps-compose-multiplatform/src/androidMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2026 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. + */ + +package com.google.maps.android.compose.multiplatform + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.maps.android.compose.rememberCameraPositionState + +@Composable +public actual fun GoogleMap( + modifier: Modifier, + latitude: Double, + longitude: Double, + zoom: Float +) { + val cameraPositionState = rememberCameraPositionState { + position = CameraPosition.fromLatLngZoom(LatLng(latitude, longitude), zoom) + } + + com.google.maps.android.compose.GoogleMap( + modifier = modifier, + cameraPositionState = cameraPositionState + ) +} diff --git a/maps-compose-multiplatform/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt b/maps-compose-multiplatform/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt new file mode 100644 index 00000000..40043bcb --- /dev/null +++ b/maps-compose-multiplatform/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt @@ -0,0 +1,32 @@ +/* + * Copyright 2026 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. + */ + +package com.google.maps.android.compose.multiplatform + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** + * A multiplatform Map Composable that renders Google Maps on Android + * and native Apple MapKit Map (MKMapView) on iOS. + */ +@Composable +public expect fun GoogleMap( + modifier: Modifier = Modifier, + latitude: Double, + longitude: Double, + zoom: Float = 10f +) diff --git a/maps-compose-multiplatform/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt b/maps-compose-multiplatform/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt new file mode 100644 index 00000000..5b578771 --- /dev/null +++ b/maps-compose-multiplatform/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2026 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. + */ + +package com.google.maps.android.compose.multiplatform + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.UIKitView +import kotlinx.cinterop.ExperimentalForeignApi +import kotlin.math.pow +import platform.CoreLocation.CLLocationCoordinate2DMake +import platform.MapKit.MKCoordinateRegionMake +import platform.MapKit.MKCoordinateSpanMake +import platform.MapKit.MKMapView + +@OptIn(ExperimentalForeignApi::class) +@Composable +public actual fun GoogleMap( + modifier: Modifier, + latitude: Double, + longitude: Double, + zoom: Float +) { + UIKitView( + factory = { + MKMapView() + }, + modifier = modifier, + update = { mapView -> + val center = CLLocationCoordinate2DMake(latitude, longitude) + // Approximate span calculation based on zoom level + val spanDelta = 360.0 / 2.0.pow(zoom.toDouble()) + val span = MKCoordinateSpanMake(spanDelta, spanDelta) + val region = MKCoordinateRegionMake(center, span) + mapView.setRegion(region, animated = true) + } + ) +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 41cc0b7e..ed79228c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -38,4 +38,5 @@ include(":maps-app") include(":maps-compose") include(":maps-compose-widgets") include(":maps-compose-utils") +include(":maps-compose-multiplatform") include(":docs") \ No newline at end of file From c134512e72c24ba033ca3c2a27ccf2ad43101569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Thu, 4 Jun 2026 09:18:33 +0200 Subject: [PATCH 2/4] feat: migrate iOS multiplatform map implementation to native Google Maps iOS SDK via CocoaPods --- maps-compose-multiplatform/build.gradle.kts | 11 ++++++++++ .../compose/multiplatform/GoogleMap.kt | 20 ++++++++----------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/maps-compose-multiplatform/build.gradle.kts b/maps-compose-multiplatform/build.gradle.kts index 9eb0110d..c721059e 100644 --- a/maps-compose-multiplatform/build.gradle.kts +++ b/maps-compose-multiplatform/build.gradle.kts @@ -16,6 +16,7 @@ plugins { kotlin("multiplatform") + kotlin("native.cocoapods") id("com.android.kotlin.multiplatform.library") alias(libs.plugins.compose.compiler) } @@ -32,6 +33,16 @@ kotlin { iosArm64() iosSimulatorArm64() + cocoapods { + summary = "Multiplatform Google Maps wrapper" + homepage = "https://github.com/googlemaps/android-maps-compose" + version = "1.0" + ios.deploymentTarget = "14.0" + pod("GoogleMaps") { + version = "9.0.0" + } + } + sourceSets { commonMain { dependencies { diff --git a/maps-compose-multiplatform/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt b/maps-compose-multiplatform/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt index 5b578771..1e8e2254 100644 --- a/maps-compose-multiplatform/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt +++ b/maps-compose-multiplatform/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt @@ -20,11 +20,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.UIKitView import kotlinx.cinterop.ExperimentalForeignApi -import kotlin.math.pow -import platform.CoreLocation.CLLocationCoordinate2DMake -import platform.MapKit.MKCoordinateRegionMake -import platform.MapKit.MKCoordinateSpanMake -import platform.MapKit.MKMapView +import kotlinx.cinterop.readValue +import platform.CoreGraphics.CGRectZero +import cocoapods.GoogleMaps.GMSMapView +import cocoapods.GoogleMaps.GMSCameraPosition @OptIn(ExperimentalForeignApi::class) @Composable @@ -36,16 +35,13 @@ public actual fun GoogleMap( ) { UIKitView( factory = { - MKMapView() + val camera = GMSCameraPosition.cameraWithLatitude(latitude, longitude, zoom) + GMSMapView.mapWithFrame(CGRectZero.readValue(), camera = camera) }, modifier = modifier, update = { mapView -> - val center = CLLocationCoordinate2DMake(latitude, longitude) - // Approximate span calculation based on zoom level - val spanDelta = 360.0 / 2.0.pow(zoom.toDouble()) - val span = MKCoordinateSpanMake(spanDelta, spanDelta) - val region = MKCoordinateRegionMake(center, span) - mapView.setRegion(region, animated = true) + val camera = GMSCameraPosition.cameraWithLatitude(latitude, longitude, zoom) + mapView.animateToCameraPosition(camera) } ) } From 8852240731ca865bfb715b23c0b0609311290c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Fri, 5 Jun 2026 17:35:55 +0200 Subject: [PATCH 3/4] feat: add interactive maps demos to iOS app & upgrade Google Maps SDK to 10.14.0 --- .gitignore | 1 + iosApp/.gitignore | 19 + iosApp/Podfile | 11 + iosApp/create_project.rb | 71 +++ iosApp/iosApp.xcodeproj/project.pbxproj | 418 ++++++++++++++++++ iosApp/iosApp/AppDelegate.swift | 25 ++ iosApp/iosApp/Info.plist | 34 ++ iosApp/iosApp/SampleListViewController.swift | 223 ++++++++++ maps-app/build.gradle.kts | 1 + maps-app/src/main/AndroidManifest.xml | 3 + .../com/google/maps/android/compose/Demo.kt | 5 + .../maps/android/compose/KmpMapActivity.kt | 49 ++ maps-app/src/main/res/values/strings.xml | 3 + maps-compose-multiplatform/build.gradle.kts | 23 +- .../maps_compose_multiplatform.podspec | 46 ++ .../compose/multiplatform/GoogleMap.kt | 46 +- .../compose/multiplatform/GoogleMap.kt | 26 +- .../compose/multiplatform/GoogleMap.kt | 120 ++++- 18 files changed, 1112 insertions(+), 12 deletions(-) create mode 100644 iosApp/.gitignore create mode 100644 iosApp/Podfile create mode 100644 iosApp/create_project.rb create mode 100644 iosApp/iosApp.xcodeproj/project.pbxproj create mode 100644 iosApp/iosApp/AppDelegate.swift create mode 100644 iosApp/iosApp/Info.plist create mode 100644 iosApp/iosApp/SampleListViewController.swift create mode 100644 maps-app/src/main/java/com/google/maps/android/compose/KmpMapActivity.kt create mode 100644 maps-compose-multiplatform/maps_compose_multiplatform.podspec diff --git a/.gitignore b/.gitignore index a77c626a..a7baf6c5 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ secrets.properties # This covers new IDEs, like Antigravity .vscode/ +.antigravitycli/ build-logic/**/bin/ diff --git a/iosApp/.gitignore b/iosApp/.gitignore new file mode 100644 index 00000000..5d5a012c --- /dev/null +++ b/iosApp/.gitignore @@ -0,0 +1,19 @@ +# CocoaPods +Pods/ +Podfile.lock + +# Xcode +build/ +DerivedData/ +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 +xcuserdata/ +*.xcworkspace +!default.xcworkspace +*.xcuserstate + +# Secrets +iosApp/DeveloperSecrets.swift + diff --git a/iosApp/Podfile b/iosApp/Podfile new file mode 100644 index 00000000..058583d9 --- /dev/null +++ b/iosApp/Podfile @@ -0,0 +1,11 @@ +platform :ios, '16.0' + +# Ignore warnings from CocoaPods dependencies +inhibit_all_warnings! + +target 'iosApp' do + use_frameworks! + + # Reference our local KMP module via its podspec path + pod 'maps_compose_multiplatform', :path => '../maps-compose-multiplatform' +end diff --git a/iosApp/create_project.rb b/iosApp/create_project.rb new file mode 100644 index 00000000..f256eae3 --- /dev/null +++ b/iosApp/create_project.rb @@ -0,0 +1,71 @@ +require 'xcodeproj' + +# Initialize project +project_path = 'iosApp.xcodeproj' +project = Xcodeproj::Project.new(project_path) + +# Set deployment target to iOS 16.0 project-wide +project.build_configurations.each do |config| + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.0' +end + +# Create group for source files (maps to the iosApp directory) +group = project.main_group.new_group('iosApp', 'iosApp') + +# Reference source files inside the group +app_delegate_ref = group.new_file('AppDelegate.swift') +sample_list_ref = group.new_file('SampleListViewController.swift') +secrets_ref = group.new_file('DeveloperSecrets.swift') +info_plist_ref = group.new_file('Info.plist') + +# Create target +target = project.new_target(:application, 'iosApp', :ios, '16.0') + +# Configure target settings +target.build_configurations.each do |config| + config.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.google.maps.android.compose.iosApp' + config.build_settings['INFOPLIST_FILE'] = 'iosApp/Info.plist' + config.build_settings['SWIFT_VERSION'] = '5.0' + config.build_settings['SDKROOT'] = 'iphoneos' + config.build_settings['TARGETED_DEVICE_FAMILY'] = '1,2' + config.build_settings['CODE_SIGNING_REQUIRED'] = 'NO' + config.build_settings['CODE_SIGNING_ALLOWED'] = 'NO' + config.build_settings['AD_HOC_CODE_SIGNING_ALLOWED'] = 'YES' +end + +# Add a Build Phase Run Script to populate secrets before compilation +populate_secrets_phase = target.new_shell_script_build_phase('Populate Secrets') +populate_secrets_phase.shell_script = <<-SHELL +SECRETS_PATH="${PROJECT_DIR}/../secrets.properties" +OUTPUT_FILE="${SRCROOT}/iosApp/DeveloperSecrets.swift" + +API_KEY="YOUR_API_KEY" + +if [ -f "$SECRETS_PATH" ]; then + EXTRACTED_KEY=$(grep -E "^MAPS_API_KEY=" "$SECRETS_PATH" | cut -d'=' -f2 | tr -d '"' | tr -d "'") + if [ ! -z "$EXTRACTED_KEY" ]; then + API_KEY="$EXTRACTED_KEY" + fi +fi + +cat < "$OUTPUT_FILE" +// Generated file. Do not commit or modify. +struct DeveloperSecrets { + static let mapsApiKey = "$API_KEY" +} +EOF +SHELL + +# Move the secrets phase to the very beginning of target build phases +target.build_phases.delete(populate_secrets_phase) +target.build_phases.insert(0, populate_secrets_phase) + +# Add files to their respective build phases +source_build_phase = target.source_build_phase +source_build_phase.add_file_reference(app_delegate_ref) +source_build_phase.add_file_reference(sample_list_ref) +source_build_phase.add_file_reference(secrets_ref) + +project.save +puts "Xcode project created successfully!" + diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 00000000..e5873b99 --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,418 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 09D299F7F5064B15DFF8647A /* SampleListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3C845D21837B44B6FED85F1 /* SampleListViewController.swift */; }; + 1937ECD4C944A623885CB9C3 /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EA2BC2B1BE1E2F248AA9D65E /* Pods_iosApp.framework */; }; + 7FE65D91E154BA74FB35FBAC /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F1DCC9BB7B6BE074CB3D1D4F /* Foundation.framework */; }; + 81C277EB5057E02FF8B0CB5C /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5293DC72FB4612B21EA96465 /* AppDelegate.swift */; }; + E997B8A8A2AFAD13A788A753 /* DeveloperSecrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3EAB5F727C7CE0015606E231 /* DeveloperSecrets.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 18EAA545799221C4749B0BE6 /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3EAB5F727C7CE0015606E231 /* DeveloperSecrets.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeveloperSecrets.swift; sourceTree = ""; }; + 5293DC72FB4612B21EA96465 /* AppDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + AFAE64BF1EC03DC7EE9905AF /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; }; + B78650FF6C736F3A8024670F /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; }; + E3C845D21837B44B6FED85F1 /* SampleListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SampleListViewController.swift; sourceTree = ""; }; + EA2BC2B1BE1E2F248AA9D65E /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F1DCC9BB7B6BE074CB3D1D4F /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + F37E4987EF58C0103DBE5753 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + ACD54AC92AF33CBA9038EF28 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7FE65D91E154BA74FB35FBAC /* Foundation.framework in Frameworks */, + 1937ECD4C944A623885CB9C3 /* Pods_iosApp.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 160662DC4B33BBB495406E79 /* Products */ = { + isa = PBXGroup; + children = ( + F37E4987EF58C0103DBE5753 /* iosApp.app */, + ); + name = Products; + sourceTree = ""; + }; + 761B8220F805A3F9823B2474 /* iOS */ = { + isa = PBXGroup; + children = ( + F1DCC9BB7B6BE074CB3D1D4F /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + 9A8CB19F0A8D45C35146683E = { + isa = PBXGroup; + children = ( + 160662DC4B33BBB495406E79 /* Products */, + AEC84CDA2E901AD18F65E5CF /* Frameworks */, + A8B082EA0D34BCDB9BC6A878 /* iosApp */, + B769C4D1E3A5E7DD227FDD0F /* Pods */, + ); + sourceTree = ""; + }; + A8B082EA0D34BCDB9BC6A878 /* iosApp */ = { + isa = PBXGroup; + children = ( + 5293DC72FB4612B21EA96465 /* AppDelegate.swift */, + E3C845D21837B44B6FED85F1 /* SampleListViewController.swift */, + 3EAB5F727C7CE0015606E231 /* DeveloperSecrets.swift */, + 18EAA545799221C4749B0BE6 /* Info.plist */, + ); + name = iosApp; + path = iosApp; + sourceTree = ""; + }; + AEC84CDA2E901AD18F65E5CF /* Frameworks */ = { + isa = PBXGroup; + children = ( + 761B8220F805A3F9823B2474 /* iOS */, + EA2BC2B1BE1E2F248AA9D65E /* Pods_iosApp.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + B769C4D1E3A5E7DD227FDD0F /* Pods */ = { + isa = PBXGroup; + children = ( + B78650FF6C736F3A8024670F /* Pods-iosApp.release.xcconfig */, + AFAE64BF1EC03DC7EE9905AF /* Pods-iosApp.debug.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 5B40C8C8EA63318DDB4B739B /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0375DD885B6888B89AAE9BF9 /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + A1495132AEB0B97D4FC21766 /* [CP] Check Pods Manifest.lock */, + 587CFD89D0C8586590AC06DD /* Populate Secrets */, + 854A57832A18FB33C8B65435 /* Sources */, + ACD54AC92AF33CBA9038EF28 /* Frameworks */, + 9FD86DF237A6365F05565BE6 /* Resources */, + 10D8D4B430DF1AE053CC752F /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iosApp; + productName = iosApp; + productReference = F37E4987EF58C0103DBE5753 /* iosApp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + E8320DC81573CAF4F46DC0D7 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + }; + buildConfigurationList = 12FCA89330D4CE38309E9B81 /* Build configuration list for PBXProject "iosApp" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 9A8CB19F0A8D45C35146683E; + minimizedProjectReferenceProxies = 0; + preferredProjectObjectVersion = 77; + productRefGroup = 160662DC4B33BBB495406E79 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5B40C8C8EA63318DDB4B739B /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 9FD86DF237A6365F05565BE6 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 10D8D4B430DF1AE053CC752F /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh", + "${PODS_CONFIGURATION_BUILD_DIR}/GoogleMaps/GoogleMapsResources.bundle", + ); + name = "[CP] Copy Pods Resources"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMapsResources.bundle", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; + 587CFD89D0C8586590AC06DD /* Populate Secrets */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Populate Secrets"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "SECRETS_PATH=\"${PROJECT_DIR}/../secrets.properties\"\nOUTPUT_FILE=\"${SRCROOT}/iosApp/DeveloperSecrets.swift\"\n\nAPI_KEY=\"YOUR_API_KEY\"\n\nif [ -f \"$SECRETS_PATH\" ]; then\n EXTRACTED_KEY=$(grep -E \"^MAPS_API_KEY=\" \"$SECRETS_PATH\" | cut -d'=' -f2 | tr -d '\"' | tr -d \"'\")\n if [ ! -z \"$EXTRACTED_KEY\" ]; then\n API_KEY=\"$EXTRACTED_KEY\"\n fi\nfi\n\ncat < \"$OUTPUT_FILE\"\n// Generated file. Do not commit or modify.\nstruct DeveloperSecrets {\n static let mapsApiKey = \"$API_KEY\"\n}\nEOF\n"; + }; + A1495132AEB0B97D4FC21766 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 854A57832A18FB33C8B65435 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 81C277EB5057E02FF8B0CB5C /* AppDelegate.swift in Sources */, + 09D299F7F5064B15DFF8647A /* SampleListViewController.swift in Sources */, + E997B8A8A2AFAD13A788A753 /* DeveloperSecrets.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 368E878A47CC5CC47D624E79 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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 = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 79144F741B9018EB7ABCBD73 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AFAE64BF1EC03DC7EE9905AF /* Pods-iosApp.debug.xcconfig */; + buildSettings = { + AD_HOC_CODE_SIGNING_ALLOWED = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGNING_ALLOWED = NO; + CODE_SIGNING_REQUIRED = NO; + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.google.maps.android.compose.iosApp; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + A18BD2CB84E1A0278F561EB6 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + 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; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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 = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + AF9933E09D62868BBCC7C26A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B78650FF6C736F3A8024670F /* Pods-iosApp.release.xcconfig */; + buildSettings = { + AD_HOC_CODE_SIGNING_ALLOWED = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGNING_ALLOWED = NO; + CODE_SIGNING_REQUIRED = NO; + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = com.google.maps.android.compose.iosApp; + SDKROOT = iphoneos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 0375DD885B6888B89AAE9BF9 /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AF9933E09D62868BBCC7C26A /* Release */, + 79144F741B9018EB7ABCBD73 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 12FCA89330D4CE38309E9B81 /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 368E878A47CC5CC47D624E79 /* Debug */, + A18BD2CB84E1A0278F561EB6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = E8320DC81573CAF4F46DC0D7 /* Project object */; +} diff --git a/iosApp/iosApp/AppDelegate.swift b/iosApp/iosApp/AppDelegate.swift new file mode 100644 index 00000000..d206fe8f --- /dev/null +++ b/iosApp/iosApp/AppDelegate.swift @@ -0,0 +1,25 @@ +import UIKit +import GoogleMaps +import maps_compose_multiplatform + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + var window: UIWindow? + + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + // Initialize Google Maps SDK dynamically. + GMSServices.provideAPIKey(DeveloperSecrets.mapsApiKey) + + window = UIWindow(frame: UIScreen.main.bounds) + let sampleListVC = SampleListViewController() + let navController = UINavigationController(rootViewController: sampleListVC) + + window?.rootViewController = navController + window?.makeKeyAndVisible() + + return true + } +} diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist new file mode 100644 index 00000000..37d0d4e6 --- /dev/null +++ b/iosApp/iosApp/Info.plist @@ -0,0 +1,34 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchScreen + + CADisableMinimumFrameDurationOnPhone + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/iosApp/iosApp/SampleListViewController.swift b/iosApp/iosApp/SampleListViewController.swift new file mode 100644 index 00000000..cc7a53a9 --- /dev/null +++ b/iosApp/iosApp/SampleListViewController.swift @@ -0,0 +1,223 @@ +import UIKit +import GoogleMaps +import maps_compose_multiplatform + +class SampleListViewController: UITableViewController { + + struct Sample { + let title: String + let description: String + let latitude: Double + let longitude: Double + let zoom: Float + let mapType: MapType + let myLocationEnabled: Bool + let scrollGesturesEnabled: Bool + let zoomGesturesEnabled: Bool + let markers: [MapMarker] + } + + struct Section { + let title: String + let samples: [Sample] + } + + private var sections: [Section] = [] + + override func viewDidLoad() { + super.viewDidLoad() + self.title = "KMP Maps Demos" + + setupSections() + self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell") + } + + private func setupSections() { + sections = [ + Section( + title: "Map Types", + samples: [ + Sample( + title: "Normal Map", + description: "Standard road map of San Francisco", + latitude: 37.7749, + longitude: -122.4194, + zoom: 12.0, + mapType: .normal, + myLocationEnabled: false, + scrollGesturesEnabled: true, + zoomGesturesEnabled: true, + markers: [] + ), + Sample( + title: "Satellite Map", + description: "Satellite imagery of the Grand Canyon", + latitude: 36.0544, + longitude: -112.1401, + zoom: 10.0, + mapType: .satellite, + myLocationEnabled: false, + scrollGesturesEnabled: true, + zoomGesturesEnabled: true, + markers: [] + ), + Sample( + title: "Hybrid Map", + description: "Satellite with road names in New York City", + latitude: 40.7128, + longitude: -74.0060, + zoom: 11.0, + mapType: .hybrid, + myLocationEnabled: false, + scrollGesturesEnabled: true, + zoomGesturesEnabled: true, + markers: [] + ), + Sample( + title: "Terrain Map", + description: "Topographic map of Mount Everest", + latitude: 27.9881, + longitude: 86.9250, + zoom: 10.0, + mapType: .terrain, + myLocationEnabled: false, + scrollGesturesEnabled: true, + zoomGesturesEnabled: true, + markers: [] + ) + ] + ), + Section( + title: "Markers & Pins", + samples: [ + Sample( + title: "Single Marker", + description: "Marker at Golden Gate Bridge", + latitude: 37.8199, + longitude: -122.4783, + zoom: 13.0, + mapType: .normal, + myLocationEnabled: false, + scrollGesturesEnabled: true, + zoomGesturesEnabled: true, + markers: [ + MapMarker(latitude: 37.8199, longitude: -122.4783, title: "Golden Gate Bridge", snippet: "San Francisco, CA") + ] + ), + Sample( + title: "Multiple Markers", + description: "Demos with Big Ben, Tower Bridge, London Eye", + latitude: 51.5033, + longitude: -0.1195, + zoom: 13.0, + mapType: .normal, + myLocationEnabled: false, + scrollGesturesEnabled: true, + zoomGesturesEnabled: true, + markers: [ + MapMarker(latitude: 51.5007, longitude: -0.1246, title: "Big Ben", snippet: "Historic Clock Tower"), + MapMarker(latitude: 51.5055, longitude: -0.0754, title: "Tower Bridge", snippet: "Famous suspension bridge"), + MapMarker(latitude: 51.5033, longitude: -0.1195, title: "London Eye", snippet: "Giant Ferris Wheel") + ] + ), + Sample( + title: "Custom Snippet Marker", + description: "Tokyo Center with descriptive marker information", + latitude: 35.6762, + longitude: 139.6503, + zoom: 11.0, + mapType: .normal, + myLocationEnabled: false, + scrollGesturesEnabled: true, + zoomGesturesEnabled: true, + markers: [ + MapMarker(latitude: 35.6762, longitude: 139.6503, title: "Tokyo City", snippet: "Population: 14 million people") + ] + ) + ] + ), + Section( + title: "Map Gestures & Controls", + samples: [ + Sample( + title: "Gestures Enabled (Default)", + description: "Fully interactive map with zoom and scroll support", + latitude: 48.8566, + longitude: 2.3522, + zoom: 12.0, + mapType: .normal, + myLocationEnabled: false, + scrollGesturesEnabled: true, + zoomGesturesEnabled: true, + markers: [] + ), + Sample( + title: "Gestures Disabled", + description: "Static map centered on Rome, Italy (Scroll & zoom locked)", + latitude: 41.9028, + longitude: 12.4964, + zoom: 12.0, + mapType: .normal, + myLocationEnabled: false, + scrollGesturesEnabled: false, + zoomGesturesEnabled: false, + markers: [] + ), + Sample( + title: "Show My Location Button", + description: "Request location services & displays Location Button", + latitude: 48.8566, + longitude: 2.3522, + zoom: 12.0, + mapType: .normal, + myLocationEnabled: true, + scrollGesturesEnabled: true, + zoomGesturesEnabled: true, + markers: [] + ) + ] + ) + ] + } + + // MARK: - Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + return sections.count + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return sections[section].samples.count + } + + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return sections[section].title + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = UITableViewCell(style: .subtitle, reuseIdentifier: "Cell") + let sample = sections[indexPath.section].samples[indexPath.row] + cell.textLabel?.text = sample.title + cell.detailTextLabel?.text = sample.description + cell.accessoryType = .disclosureIndicator + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let sample = sections[indexPath.section].samples[indexPath.row] + + let mapViewController = GoogleMapKt.GoogleMapViewController( + latitude: sample.latitude, + longitude: sample.longitude, + zoom: sample.zoom, + mapType: sample.mapType, + myLocationEnabled: sample.myLocationEnabled, + scrollGesturesEnabled: sample.scrollGesturesEnabled, + zoomGesturesEnabled: sample.zoomGesturesEnabled, + markers: sample.markers + ) + + mapViewController.title = sample.title + self.navigationController?.pushViewController(mapViewController, animated: true) + } +} diff --git a/maps-app/build.gradle.kts b/maps-app/build.gradle.kts index f76710f4..3a2555db 100644 --- a/maps-app/build.gradle.kts +++ b/maps-app/build.gradle.kts @@ -134,6 +134,7 @@ dependencies { implementation(project(":maps-compose")) implementation(project(":maps-compose-widgets")) implementation(project(":maps-compose-utils")) + implementation(project(":maps-compose-multiplatform")) } secrets { diff --git a/maps-app/src/main/AndroidManifest.xml b/maps-app/src/main/AndroidManifest.xml index d312b735..5d5663b0 100644 --- a/maps-app/src/main/AndroidManifest.xml +++ b/maps-app/src/main/AndroidManifest.xml @@ -115,6 +115,9 @@ + diff --git a/maps-app/src/main/java/com/google/maps/android/compose/Demo.kt b/maps-app/src/main/java/com/google/maps/android/compose/Demo.kt index 2931e60f..0c32a6d7 100644 --- a/maps-app/src/main/java/com/google/maps/android/compose/Demo.kt +++ b/maps-app/src/main/java/com/google/maps/android/compose/Demo.kt @@ -85,6 +85,11 @@ sealed class ActivityGroup( R.string.street_view_activity_description, StreetViewActivity::class ), + Activity( + R.string.kmp_map_activity, + R.string.kmp_map_activity_description, + KmpMapActivity::class + ), ) ) diff --git a/maps-app/src/main/java/com/google/maps/android/compose/KmpMapActivity.kt b/maps-app/src/main/java/com/google/maps/android/compose/KmpMapActivity.kt new file mode 100644 index 00000000..37c616e6 --- /dev/null +++ b/maps-app/src/main/java/com/google/maps/android/compose/KmpMapActivity.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2026 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. + */ + +package com.google.maps.android.compose + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.ui.Modifier +import com.google.maps.android.compose.multiplatform.GoogleMap +import com.google.maps.android.compose.multiplatform.MapMarker + +class KmpMapActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + // Renders the multiplatform Map Composable + GoogleMap( + modifier = Modifier.fillMaxSize(), + latitude = 37.7749, // San Francisco + longitude = -122.4194, + zoom = 12f, + markers = listOf( + MapMarker( + latitude = 37.7749, + longitude = -122.4194, + title = "San Francisco", + snippet = "Welcome to SF!" + ) + ) + ) + } + } +} + diff --git a/maps-app/src/main/res/values/strings.xml b/maps-app/src/main/res/values/strings.xml index 1147de14..6e452257 100644 --- a/maps-app/src/main/res/values/strings.xml +++ b/maps-app/src/main/res/values/strings.xml @@ -81,6 +81,9 @@ Ground Overlay Adding a ground overlay to the map. + KMP Multiplatform Map + A map showcasing the KMP GoogleMap wrapper. + Map Types Map Features diff --git a/maps-compose-multiplatform/build.gradle.kts b/maps-compose-multiplatform/build.gradle.kts index c721059e..0a24de58 100644 --- a/maps-compose-multiplatform/build.gradle.kts +++ b/maps-compose-multiplatform/build.gradle.kts @@ -37,9 +37,28 @@ kotlin { summary = "Multiplatform Google Maps wrapper" homepage = "https://github.com/googlemaps/android-maps-compose" version = "1.0" - ios.deploymentTarget = "14.0" + ios.deploymentTarget = "16.0" pod("GoogleMaps") { - version = "9.0.0" + version = "10.14.0.0" + } + framework { + baseName = "maps_compose_multiplatform" + isStatic = true + linkerOpts("-lc++") + linkerOpts("-framework", "Accelerate") + linkerOpts("-framework", "CoreData") + linkerOpts("-framework", "CoreGraphics") + linkerOpts("-framework", "CoreImage") + linkerOpts("-framework", "CoreLocation") + linkerOpts("-framework", "CoreText") + linkerOpts("-framework", "GLKit") + linkerOpts("-framework", "ImageIO") + linkerOpts("-framework", "Metal") + linkerOpts("-framework", "OpenGLES") + linkerOpts("-framework", "QuartzCore") + linkerOpts("-framework", "Security") + linkerOpts("-framework", "SystemConfiguration") + linkerOpts("-framework", "UIKit") } } diff --git a/maps-compose-multiplatform/maps_compose_multiplatform.podspec b/maps-compose-multiplatform/maps_compose_multiplatform.podspec new file mode 100644 index 00000000..86cfb6f6 --- /dev/null +++ b/maps-compose-multiplatform/maps_compose_multiplatform.podspec @@ -0,0 +1,46 @@ +Pod::Spec.new do |spec| + spec.name = 'maps_compose_multiplatform' + spec.version = '1.0' + spec.homepage = 'https://github.com/googlemaps/android-maps-compose' + spec.source = { :http=> ''} + spec.authors = '' + spec.license = '' + spec.summary = 'Multiplatform Google Maps wrapper' + spec.vendored_frameworks = 'build/cocoapods/framework/maps_compose_multiplatform.framework' + spec.libraries = 'c++' + spec.ios.deployment_target = '15.0' + spec.dependency 'GoogleMaps', '10.14.0.0' + if !Dir.exist?('build/cocoapods/framework/maps_compose_multiplatform.framework') || Dir.empty?('build/cocoapods/framework/maps_compose_multiplatform.framework') + raise " + Kotlin framework 'maps_compose_multiplatform' doesn't exist yet, so a proper Xcode project can't be generated. + 'pod install' should be executed after running ':generateDummyFramework' Gradle task: + ./gradlew :maps-compose-multiplatform:generateDummyFramework + Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)" + end + spec.xcconfig = { + 'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO', + } + spec.pod_target_xcconfig = { + 'KOTLIN_PROJECT_PATH' => ':maps-compose-multiplatform', + 'PRODUCT_MODULE_NAME' => 'maps_compose_multiplatform', + } + spec.script_phases = [ + { + :name => 'Build maps_compose_multiplatform', + :execution_position => :before_compile, + :shell_path => '/bin/sh', + :script => <<-SCRIPT + if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then + echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"" + exit 0 + fi + set -ev + REPO_ROOT="$PODS_TARGET_SRCROOT" + "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \ + -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \ + -Pkotlin.native.cocoapods.archs="$ARCHS" \ + -Pkotlin.native.cocoapods.configuration="$CONFIGURATION" + SCRIPT + } + ] +end diff --git a/maps-compose-multiplatform/src/androidMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt b/maps-compose-multiplatform/src/androidMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt index d2696474..56460553 100644 --- a/maps-compose-multiplatform/src/androidMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt +++ b/maps-compose-multiplatform/src/androidMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt @@ -17,6 +17,7 @@ package com.google.maps.android.compose.multiplatform import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import com.google.android.gms.maps.model.CameraPosition import com.google.android.gms.maps.model.LatLng @@ -27,14 +28,53 @@ public actual fun GoogleMap( modifier: Modifier, latitude: Double, longitude: Double, - zoom: Float + zoom: Float, + mapType: MapType, + myLocationEnabled: Boolean, + scrollGesturesEnabled: Boolean, + zoomGesturesEnabled: Boolean, + markers: List ) { val cameraPositionState = rememberCameraPositionState { position = CameraPosition.fromLatLngZoom(LatLng(latitude, longitude), zoom) } + val mapProperties = remember(mapType, myLocationEnabled) { + com.google.maps.android.compose.MapProperties( + mapType = when (mapType) { + MapType.NONE -> com.google.maps.android.compose.MapType.NONE + MapType.NORMAL -> com.google.maps.android.compose.MapType.NORMAL + MapType.SATELLITE -> com.google.maps.android.compose.MapType.SATELLITE + MapType.TERRAIN -> com.google.maps.android.compose.MapType.TERRAIN + MapType.HYBRID -> com.google.maps.android.compose.MapType.HYBRID + }, + isMyLocationEnabled = myLocationEnabled + ) + } + + val mapUiSettings = remember(scrollGesturesEnabled, zoomGesturesEnabled) { + com.google.maps.android.compose.MapUiSettings( + scrollGesturesEnabled = scrollGesturesEnabled, + zoomGesturesEnabled = zoomGesturesEnabled + ) + } + com.google.maps.android.compose.GoogleMap( modifier = modifier, - cameraPositionState = cameraPositionState - ) + cameraPositionState = cameraPositionState, + properties = mapProperties, + uiSettings = mapUiSettings + ) { + markers.forEach { markerData -> + val markerState = com.google.maps.android.compose.rememberMarkerState( + position = LatLng(markerData.latitude, markerData.longitude) + ) + com.google.maps.android.compose.Marker( + state = markerState, + title = markerData.title, + snippet = markerData.snippet + ) + } + } } + diff --git a/maps-compose-multiplatform/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt b/maps-compose-multiplatform/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt index 40043bcb..6be844a7 100644 --- a/maps-compose-multiplatform/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt +++ b/maps-compose-multiplatform/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt @@ -19,14 +19,34 @@ package com.google.maps.android.compose.multiplatform import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +public enum class MapType { + NONE, + NORMAL, + SATELLITE, + TERRAIN, + HYBRID +} + +public data class MapMarker( + val latitude: Double, + val longitude: Double, + val title: String? = null, + val snippet: String? = null +) + /** - * A multiplatform Map Composable that renders Google Maps on Android - * and native Apple MapKit Map (MKMapView) on iOS. + * A multiplatform Map Composable that renders Google Maps on Android and iOS. */ @Composable public expect fun GoogleMap( modifier: Modifier = Modifier, latitude: Double, longitude: Double, - zoom: Float = 10f + zoom: Float = 10f, + mapType: MapType = MapType.NORMAL, + myLocationEnabled: Boolean = false, + scrollGesturesEnabled: Boolean = true, + zoomGesturesEnabled: Boolean = true, + markers: List = emptyList() ) + diff --git a/maps-compose-multiplatform/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt b/maps-compose-multiplatform/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt index 1e8e2254..72f7365e 100644 --- a/maps-compose-multiplatform/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt +++ b/maps-compose-multiplatform/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt @@ -22,16 +22,28 @@ import androidx.compose.ui.viewinterop.UIKitView import kotlinx.cinterop.ExperimentalForeignApi import kotlinx.cinterop.readValue import platform.CoreGraphics.CGRectZero -import cocoapods.GoogleMaps.GMSMapView -import cocoapods.GoogleMaps.GMSCameraPosition +import cocoapods.GoogleMaps.* -@OptIn(ExperimentalForeignApi::class) +import platform.UIKit.UIViewController +import androidx.compose.ui.window.ComposeUIViewController +import androidx.compose.foundation.layout.fillMaxSize + +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitInteropInteractionMode +import platform.CoreLocation.CLLocationCoordinate2DMake + +@OptIn(ExperimentalForeignApi::class, androidx.compose.ui.ExperimentalComposeUiApi::class) @Composable public actual fun GoogleMap( modifier: Modifier, latitude: Double, longitude: Double, - zoom: Float + zoom: Float, + mapType: MapType, + myLocationEnabled: Boolean, + scrollGesturesEnabled: Boolean, + zoomGesturesEnabled: Boolean, + markers: List ) { UIKitView( factory = { @@ -39,9 +51,109 @@ public actual fun GoogleMap( GMSMapView.mapWithFrame(CGRectZero.readValue(), camera = camera) }, modifier = modifier, + properties = UIKitInteropProperties( + interactionMode = UIKitInteropInteractionMode.NonCooperative + ), update = { mapView -> val camera = GMSCameraPosition.cameraWithLatitude(latitude, longitude, zoom) mapView.animateToCameraPosition(camera) + + // Update map type + mapView.mapType = when (mapType) { + MapType.NONE -> kGMSTypeNone + MapType.NORMAL -> kGMSTypeNormal + MapType.SATELLITE -> kGMSTypeSatellite + MapType.TERRAIN -> kGMSTypeTerrain + MapType.HYBRID -> kGMSTypeHybrid + } + + // Update my location + mapView.myLocationEnabled = myLocationEnabled + mapView.settings.myLocationButton = myLocationEnabled + + // Update gestures + mapView.settings.scrollGestures = scrollGesturesEnabled + mapView.settings.zoomGestures = zoomGesturesEnabled + + // Clear old and add new markers + mapView.clear() + markers.forEach { markerData -> + val marker = GMSMarker() + marker.position = CLLocationCoordinate2DMake(markerData.latitude, markerData.longitude) + marker.title = markerData.title + marker.snippet = markerData.snippet + marker.map = mapView + } } ) } + +public fun GoogleMapViewController( + latitude: Double, + longitude: Double, + zoom: Float +): UIViewController { + return GoogleMapViewController( + latitude = latitude, + longitude = longitude, + zoom = zoom, + mapType = MapType.NORMAL, + myLocationEnabled = false, + scrollGesturesEnabled = true, + zoomGesturesEnabled = true, + markers = emptyList() + ) +} + +public fun GoogleMapViewController( + latitude: Double, + longitude: Double, + zoom: Float, + markerLatitude: Double?, + markerLongitude: Double?, + markerTitle: String? +): UIViewController { + val markersList = if (markerLatitude != null && markerLongitude != null) { + listOf(MapMarker(latitude = markerLatitude, longitude = markerLongitude, title = markerTitle)) + } else { + emptyList() + } + return GoogleMapViewController( + latitude = latitude, + longitude = longitude, + zoom = zoom, + mapType = MapType.NORMAL, + myLocationEnabled = false, + scrollGesturesEnabled = true, + zoomGesturesEnabled = true, + markers = markersList + ) +} + +public fun GoogleMapViewController( + latitude: Double, + longitude: Double, + zoom: Float, + mapType: MapType, + myLocationEnabled: Boolean, + scrollGesturesEnabled: Boolean, + zoomGesturesEnabled: Boolean, + markers: List +): UIViewController { + return ComposeUIViewController(configure = { + enforceStrictPlistSanityCheck = false + }) { + GoogleMap( + modifier = Modifier.fillMaxSize(), + latitude = latitude, + longitude = longitude, + zoom = zoom, + mapType = mapType, + myLocationEnabled = myLocationEnabled, + scrollGesturesEnabled = scrollGesturesEnabled, + zoomGesturesEnabled = zoomGesturesEnabled, + markers = markers + ) + } +} + From f2b769f0be377822e704804c9590d4a45bd1f396 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Enrique=20Lo=CC=81pez=20Man=CC=83as?= Date: Fri, 5 Jun 2026 18:16:42 +0200 Subject: [PATCH 4/4] fix: resolve YAML syntax error in auto-fix.yml workflow --- .github/workflows/auto-fix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/auto-fix.yml b/.github/workflows/auto-fix.yml index 6fe2c52c..c505fc70 100644 --- a/.github/workflows/auto-fix.yml +++ b/.github/workflows/auto-fix.yml @@ -31,7 +31,7 @@ on: jobs: auto-fix: - if: github.event.label.name == 'agent: create-pr' || github.event_name == 'workflow_dispatch' + if: "${{ github.event.label.name == 'agent: create-pr' || github.event_name == 'workflow_dispatch' }}" runs-on: ubuntu-latest permissions: contents: write