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 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/BUILD_KMP.md b/BUILD_KMP.md new file mode 100644 index 00000000..a68effca --- /dev/null +++ b/BUILD_KMP.md @@ -0,0 +1,83 @@ +# Kotlin Multiplatform (KMP) Maps Demo — Build & Run Guide + +This guide details how to build and run the multiplatform Maps Compose library (`:maps-compose-multiplatform`), the Android demo app (`:maps-app`), and the iOS demo app (`iosApp`). + +--- + +## 1. Checkout the PR / Branch + +Fetch the remote branch and check it out (or add it as a Git worktree): + +```bash +git fetch origin feat/experimental-kmp-module +git checkout feat/experimental-kmp-module +``` + +*(Alternatively, to isolate your work in a worktree branch)*: +```bash +git worktree add -b feat/experimental-kmp-module feat-experimental-kmp-module origin/feat/experimental-kmp-module +``` + +--- + +## 2. Configure Local API Keys + +Create a file named `secrets.properties` in the **root** directory of the repository (if you don't already have one) and populate it with your Google Maps API keys: + +```properties +MAPS_API_KEY=YOUR_GOOGLE_MAPS_API_KEY +PLACES_API_KEY=YOUR_GOOGLE_PLACES_API_KEY +``` + +> [!IMPORTANT] +> Both `secrets.properties` and the generated iOS equivalent (`DeveloperSecrets.swift`) are ignored by Git to prevent accidentally leaking credentials. + +--- + +## 3. Building & Running for Android + +To compile and verify both the KMP library Android target and the Android sample application (`:maps-app`), run: + +```bash +./gradlew assembleDebug +``` + +--- + +## 4. Building & Running for iOS (Step-by-Step) + +If you are running the iOS project (`iosApp`) for the first time, follow these steps on a Mac with **Xcode** and **CocoaPods** installed: + +### Step 4.1: Generate the KMP Dummy Framework +Before CocoaPods can link the local `:maps-compose-multiplatform` podspec, the initial Kotlin framework must exist. From the repository root, execute: + +```bash +./gradlew :maps-compose-multiplatform:generateDummyFramework +``` + +### Step 4.2: Generate the Xcode Project +Navigate to the `iosApp` directory and run the project generation Ruby script (requires the `xcodeproj` gem): + +```bash +cd iosApp +ruby create_project.rb +``` + +> [!NOTE] +> **Build System Output Tracking (Xcode 16+)**: The `create_project.rb` script declares `$(SRCROOT)/iosApp/DeveloperSecrets.swift` as an output of the **Populate Secrets** script build phase. This prevents modern Xcode build systems from failing with `Build input file cannot be found: DeveloperSecrets.swift` during dependency graph analysis. + +### Step 4.3: Install Pod Dependencies +Install the required CocoaPods (`GoogleMaps` 10.14.0 and the local KMP library). If your local specs repository is out of date, use the `--repo-update` flag: + +```bash +pod install --repo-update +``` + +### Step 4.4: Open and Run in Xcode +1. Open the generated **Workspace** (do **not** open the `.xcodeproj` file directly): + ```bash + open iosApp.xcworkspace + ``` +2. In Xcode's top toolbar, select the active scheme dropdown and set it to **`iosApp`** (instead of `maps_compose_multiplatform`). +3. Choose an iOS Simulator (e.g., **iPhone 16** or **iPhone 17 Pro**) in the destination dropdown. +4. Click the **Play** button (or press **Cmd + R**) to compile, run the secrets population script, and launch the app! 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..cf90bfb1 --- /dev/null +++ b/iosApp/create_project.rb @@ -0,0 +1,72 @@ +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.output_paths = ['$(SRCROOT)/iosApp/DeveloperSecrets.swift'] +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..c8ac885e --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,419 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0D653230C7B4B77DED78F795 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B55B65E85F954E4BF49E7CA /* AppDelegate.swift */; }; + 5520C2B8920F1DE6E2146F81 /* SampleListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75DDC1C063EA32CE8824D07F /* SampleListViewController.swift */; }; + 7DE9F50AB020D738F9AC35AF /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AB792F01E14CD3EEC1AF5B8 /* Pods_iosApp.framework */; }; + 9B39CDB0583E7EFC46398A00 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D66C3B8A1626E64624FFC75 /* Foundation.framework */; }; + C048757D5C4B50BC1AB38AE6 /* DeveloperSecrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4DBA12C4C8AAC40F8EE45 /* DeveloperSecrets.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2E61E10ADF7CF13E32E000C0 /* 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 = ""; }; + 4D66C3B8A1626E64624FFC75 /* 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; }; + 51C4DBA12C4C8AAC40F8EE45 /* DeveloperSecrets.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeveloperSecrets.swift; sourceTree = ""; }; + 6B55B65E85F954E4BF49E7CA /* AppDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 711AFBB8CF132FD69AED5EAE /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 75DDC1C063EA32CE8824D07F /* SampleListViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SampleListViewController.swift; sourceTree = ""; }; + 7AB792F01E14CD3EEC1AF5B8 /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7FA1089A5A50A88C1B0534CC /* 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 = ""; }; + B84637CCA3496DDB1F800E10 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 755A4F11212F18CBAE1A61A0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9B39CDB0583E7EFC46398A00 /* Foundation.framework in Frameworks */, + 7DE9F50AB020D738F9AC35AF /* Pods_iosApp.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6572DE9AD86B0CC67C7E963E /* Frameworks */ = { + isa = PBXGroup; + children = ( + C469B336BFDB55F88E4144D1 /* iOS */, + 7AB792F01E14CD3EEC1AF5B8 /* Pods_iosApp.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 8F4470E4F5DD69A5F2052318 /* iosApp */ = { + isa = PBXGroup; + children = ( + 6B55B65E85F954E4BF49E7CA /* AppDelegate.swift */, + 75DDC1C063EA32CE8824D07F /* SampleListViewController.swift */, + 51C4DBA12C4C8AAC40F8EE45 /* DeveloperSecrets.swift */, + 711AFBB8CF132FD69AED5EAE /* Info.plist */, + ); + name = iosApp; + path = iosApp; + sourceTree = ""; + }; + 9C595BA209CE46C978747670 = { + isa = PBXGroup; + children = ( + F5812B1B51C667A99E3473DB /* Products */, + 6572DE9AD86B0CC67C7E963E /* Frameworks */, + 8F4470E4F5DD69A5F2052318 /* iosApp */, + C21DC034EFC3BBCE3CB62ECF /* Pods */, + ); + sourceTree = ""; + }; + C21DC034EFC3BBCE3CB62ECF /* Pods */ = { + isa = PBXGroup; + children = ( + 7FA1089A5A50A88C1B0534CC /* Pods-iosApp.release.xcconfig */, + 2E61E10ADF7CF13E32E000C0 /* Pods-iosApp.debug.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + C469B336BFDB55F88E4144D1 /* iOS */ = { + isa = PBXGroup; + children = ( + 4D66C3B8A1626E64624FFC75 /* Foundation.framework */, + ); + name = iOS; + sourceTree = ""; + }; + F5812B1B51C667A99E3473DB /* Products */ = { + isa = PBXGroup; + children = ( + B84637CCA3496DDB1F800E10 /* iosApp.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + F5CA17578FAB66DC77D30B26 /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = CF3EFFA659DA87ADF491844D /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + ABA2E729135D4636D5EBF68A /* [CP] Check Pods Manifest.lock */, + EA513F9C2ABA8D7F7B488510 /* Populate Secrets */, + C8810296C81A419006D89E5E /* Sources */, + 755A4F11212F18CBAE1A61A0 /* Frameworks */, + C6F77023AAF8823528A70ED2 /* Resources */, + D63BC68F2C76415CBAB80FEA /* [CP] Copy Pods Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iosApp; + productName = iosApp; + productReference = B84637CCA3496DDB1F800E10 /* iosApp.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0BAC57553145A9D99D214F34 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + }; + buildConfigurationList = 2F8B9FD9291D2E34BAB2808B /* Build configuration list for PBXProject "iosApp" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 9C595BA209CE46C978747670; + minimizedProjectReferenceProxies = 0; + preferredProjectObjectVersion = 77; + productRefGroup = F5812B1B51C667A99E3473DB /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + F5CA17578FAB66DC77D30B26 /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + C6F77023AAF8823528A70ED2 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + ABA2E729135D4636D5EBF68A /* [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; + }; + D63BC68F2C76415CBAB80FEA /* [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; + }; + EA513F9C2ABA8D7F7B488510 /* Populate Secrets */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Populate Secrets"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(SRCROOT)/iosApp/DeveloperSecrets.swift", + ); + 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"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + C8810296C81A419006D89E5E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0D653230C7B4B77DED78F795 /* AppDelegate.swift in Sources */, + 5520C2B8920F1DE6E2146F81 /* SampleListViewController.swift in Sources */, + C048757D5C4B50BC1AB38AE6 /* DeveloperSecrets.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 30D83033DD758DCAE2B65F37 /* 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; + }; + 79F5FD2A2E53F2F285DB566F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7FA1089A5A50A88C1B0534CC /* 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; + }; + 911DF9EA0DD218A18147B490 /* 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; + }; + BE70C656E6CD2E4A26E99D95 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2E61E10ADF7CF13E32E000C0 /* 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; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2F8B9FD9291D2E34BAB2808B /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 911DF9EA0DD218A18147B490 /* Debug */, + 30D83033DD758DCAE2B65F37 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CF3EFFA659DA87ADF491844D /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 79F5FD2A2E53F2F285DB566F /* Release */, + BE70C656E6CD2E4A26E99D95 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0BAC57553145A9D99D214F34 /* 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 new file mode 100644 index 00000000..0a24de58 --- /dev/null +++ b/maps-compose-multiplatform/build.gradle.kts @@ -0,0 +1,85 @@ +/* + * 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") + kotlin("native.cocoapods") + 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() + + cocoapods { + summary = "Multiplatform Google Maps wrapper" + homepage = "https://github.com/googlemaps/android-maps-compose" + version = "1.0" + ios.deploymentTarget = "16.0" + pod("GoogleMaps") { + 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") + } + } + + 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/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 new file mode 100644 index 00000000..56460553 --- /dev/null +++ b/maps-compose-multiplatform/src/androidMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt @@ -0,0 +1,80 @@ +/* + * 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.runtime.remember +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, + 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, + 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 new file mode 100644 index 00000000..6be844a7 --- /dev/null +++ b/maps-compose-multiplatform/src/commonMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt @@ -0,0 +1,52 @@ +/* + * 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 + +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 iOS. + */ +@Composable +public expect fun GoogleMap( + modifier: Modifier = Modifier, + latitude: Double, + longitude: Double, + 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 new file mode 100644 index 00000000..72f7365e --- /dev/null +++ b/maps-compose-multiplatform/src/iosMain/kotlin/com/google/maps/android/compose/multiplatform/GoogleMap.kt @@ -0,0 +1,159 @@ +/* + * 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 kotlinx.cinterop.readValue +import platform.CoreGraphics.CGRectZero +import cocoapods.GoogleMaps.* + +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, + mapType: MapType, + myLocationEnabled: Boolean, + scrollGesturesEnabled: Boolean, + zoomGesturesEnabled: Boolean, + markers: List +) { + UIKitView( + factory = { + val camera = GMSCameraPosition.cameraWithLatitude(latitude, longitude, zoom) + 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 + ) + } +} + 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