capacitor-plugins icon indicating copy to clipboard operation
capacitor-plugins copied to clipboard

Google Maps not loading on iOS (physical device)

Open judge opened this issue 2 years ago • 27 comments

Bug Report

Plugin(s)

Google Maps

Capacitor Version

💊   Capacitor Doctor  💊 

Latest Dependencies:

  @capacitor/cli: 4.6.1
  @capacitor/core: 4.6.1
  @capacitor/android: 4.6.1
  @capacitor/ios: 4.6.1

Installed Dependencies:

  @capacitor/android: not installed
  @capacitor/cli: 4.6.1
  @capacitor/core: 4.6.1
  @capacitor/ios: 4.6.1

[success] iOS looking great! 👌

Platform(s)

iOS

Current Behavior

Empty box is appearing instead of Google Maps. Web works perfectly.

Expected Behavior

Show the map.

Code Reproduction

Sample webcomponent (apiKey deleted):

import { GoogleMap } from '@capacitor/google-maps';

const apiKey = '';

window.customElements.define('sample-app', class extends HTMLElement {
  async connectedCallback() {
    this.innerHTML = `
      <style>
        capacitor-google-map {
          display: inline-block;
          width: 200px;
          height: 400px;
        }
      </style>
      <capacitor-google-map id="main-map" style="border: 1px solid red;"></capacitor-google-map>
    `;

    const mapRef = this.querySelector('#main-map');

    const newMap = await GoogleMap.create({
      id: 'my-map',
      forceCreate: true,
      element: mapRef,
      apiKey: apiKey,
      config: {
        center: {
          lat: 33.6,
          lng: -117.9,
        },
        zoom: 8,
      },
    });
  }
});

Other Technical Details

When I load the page on web it works fine, on iOS (physical device) I can see an empty box (with the red border I added).

Additional Context

There is no error at all, I can see the following in XCode:

⚡️  [log] - [vite] connected.
⚡️  WebView loaded
⚡️  To Native ->  CapacitorGoogleMaps addListener 26518793
⚡️  To Native ->  CapacitorGoogleMaps create 26518794
⚡️  TO JS undefined

judge avatar Jan 06 '23 14:01 judge

This issue may need more information before it can be addressed. In particular, it will need a reliable Code Reproduction that demonstrates the issue.

Please see the Contributing Guide for how to create a Code Reproduction.

Thanks! Ionitron 💙

Ionitron avatar Jan 09 '23 10:01 Ionitron

can you change the id to main-map and let me know if the problem still exists?

    const newMap = await GoogleMap.create({
      id: 'main-map', // <--
      forceCreate: true,
      element: mapRef,
      apiKey: apiKey,
      config: {
        center: {
          lat: 33.6,
          lng: -117.9,
        },
        zoom: 8,
      },
    });

I was able to render map on ios using this snippet

DwieDima avatar Jan 09 '23 14:01 DwieDima

Hi @DwieDima , I cannot see the map after the proposed change. :( I uploaded a sample application: https://github.com/judge/sample-app Thanks!

judge avatar Jan 09 '23 15:01 judge

Hi @judge ,

Did you found any work around for this issue?

naqeeb-klabs avatar Jan 17 '23 07:01 naqeeb-klabs

Unfortunetaly not. I would add "needs reply" and remove "needs reproduction" labels to the issue but I cannot do that. :(

judge avatar Jan 18 '23 12:01 judge

Hi @judge ,

Did you enabled Maps SDK for iOS ? https://developers.google.com/maps/documentation/ios-sdk/cloud-setup#enabling-apis

akeeee avatar Feb 06 '23 09:02 akeeee

Hi @akeeee ,

Yes it is enabled for both JavaScript and iOS. The JavaScript API works perfectly, it even shows request count but there is no request count for iOS.

judge avatar Feb 07 '23 05:02 judge

It seems that the ScrollView the plugin is searching for is sometimes not existing yet when the map is initialized. I worked around this by changing Map.swift like this:

if let target = self.targetViewController {
    target.tag = 1
    target.removeAllSubview()
    self.mapViewController.view.frame = target.bounds
    target.addSubview(self.mapViewController.view)
    self.mapViewController.GMapView.delegate = self.delegate
} else { // add this else case
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
        self.render(callback)
    }
    return;
}

If you also need your callbacks to wait until the map is created you need to call the resolve at the point that onMapReady is signaled.

Simon54 avatar Feb 13 '23 12:02 Simon54

I'm also facing this issue exactly.

On first use of the map, the "box" and the marker loads, but there's no actual map.

If I navigate to another screen — even if that screen is using maps — I get nothing.

It does work on web; I get the same logs @judge is getting from XCode.

Using:

💊 Capacitor Doctor 💊

Latest Dependencies:

@capacitor/cli: 4.6.3 @capacitor/core: 4.6.3 @capacitor/android: 4.6.3 @capacitor/ios: 4.6.3

Installed Dependencies:

@capacitor/android: not installed @capacitor/cli: 4.6.3 @capacitor/core: 4.6.3 @capacitor/ios: 4.6.3

yoaquim avatar Feb 22 '23 02:02 yoaquim

@judge where you able to find a solution?

yoaquim avatar Feb 22 '23 15:02 yoaquim

@Simon54 thank you - that sorted it out for me. I'll now have to make sure this "patch" gets applied when this is deployed to CI.

avioli avatar May 18 '23 03:05 avioli

Bump!

metinjakupi avatar May 18 '23 10:05 metinjakupi

Ok... I think I found a better solution than patching Map.swift, which was a hit-or-miss solution anyway.

What I'm doing and is consistently working is:

  1. I wrap the capacitor-google-map element in a relative div:
<template>
  <div class="map-wrapper">
    <capacitor-google-map id="map"></capacitor-google-map>
  </div>
</template>
  1. Then I make the capacitor-google-map an absolutely positioned element:
<style scoped>
.map-wrapper {
  position: relative;
  flex: 1;
}

capacitor-google-map {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
</style>

(I use this component within another that uses display: flex; so I have flex: 1 to ensure the wrapper grows to maximum height on mobile)

  1. Then in my script I wait for the capacitor-google-map to be connected with a simple setTimeout... AND IT WORKS EVERY TIME:
<script setup lang="ts">
import { GoogleMap } from '@capacitor/google-maps';
import { onBeforeUnmount } from 'vue';
import { onMounted } from 'vue';

const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;

let map: GoogleMap | null = null;

onMounted(async () => {
  await new Promise(res => setTimeout(res, 300));

  const mapRef = document.getElementById('map')!;

  const newMap = await GoogleMap.create({
    id: `my-map`, // Unique identifier for this map instance
    element: mapRef!, // reference to the capacitor-google-map element
    apiKey: apiKey, // Your Google Maps API Key
    config: {
      center: { lat: 33.6, lng: -117.9 },
      zoom: 8,
    },
  });
  map = newMap;
});

onBeforeUnmount(() => {
  if (map) {
    map.destroy();
    map = null;
  }
})

I did so many... many, many, many iterations with just a single change and this is the only solution without any changes to the plugin itself that worked!

Key findings:

  • The capacitor-google-map is a Custom Element, which takes a tiny bit of time to be hooked to its JS element after the <capacitor-google-map /> element is in the DOM. That JS connection adds an additional DIV element inside it that has 200% height, so this makes a ScollView in the WebKit browser.
  • If that ScrollView doesn't get created - this plugin cannot find and attach the GoogleMaps view controller and silently fails!!!
  • The Map.swift file makes a lookup for the said ScrollView by using some dodgy logic, but it works if everything is just right.
  • The container for the capacitor-google-map element must not be a Flex box and it must allow overflowing - otherwise the ScrollView won't be created by WebKit. Even if some parent container is a Flex box - the immediate parent should allow overflowing, so the 200% high DIV can ensure the ScrollView is created! So... your best bet is to use an absolutely positioned style for the bloody capacitor-google-map and wrap it in a relatively positioned DIV, which you can size as your heart desires.

That's it form me and good luck.

avioli avatar May 18 '23 13:05 avioli

@judge I know this finding is some five months late, but to make my solution work in your context, since I use Vue and you do not - add a timeout before calling GoogleMap.create to give a chance to capacitor-google-map to connect properly.

I personally wasn't able to make it work with the styles suggested in the official "documentation" - I had to use a wrapper DIV, so in your case I would suggest you do:

this.innerHTML = `
  <style>
    #map-wrapper {
      display: inline-block;
      width: 200px;
      height: 400px;
      position: relative;
    }
    capacitor-google-map {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
  </style>
  <div id="map-wrapper" style="border: 1px solid red;">
    <capacitor-google-map id="main-map"></capacitor-google-map>
  </div>
`;

I tested the above wrapper styles within my Vue app and it worked for me, but I had to move the red border to the wrapper - otherwise the dodgy logic in Maps.swift fails to identify the ScrollView :)

avioli avatar May 18 '23 13:05 avioli

It seems that the ScrollView the plugin is searching for is sometimes not existing yet when the map is initialized. I worked around this by changing Map.swift like this:

if let target = self.targetViewController {
    target.tag = 1
    target.removeAllSubview()
    self.mapViewController.view.frame = target.bounds
    target.addSubview(self.mapViewController.view)
    self.mapViewController.GMapView.delegate = self.delegate
} else { // add this else case
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
        self.render(callback)
    }
    return;
}

If you also need your callbacks to wait until the map is created you need to call the resolve at the point that onMapReady is signaled.

Can you give full example please?

metinjakupi avatar Jun 05 '23 07:06 metinjakupi

@metinjakupi The <capacitor-google-map> element is a custom element, which requires an event-loop cycle for the browser to connect it to JavaScript once the DOM is updated. Once that's connected and if the *ScrollView exists the lookup for the targetViewController will succeed. Otherwise it may need a bit more time.

Unfortunately if the *ScrollView is never created by Webkit - that loop will go forever.

I did patch the swift file in the hope that would be enough of a fix, but it wasn't and I felt it is pretty bad - patching and all.

avioli avatar Jun 05 '23 23:06 avioli

https://github.com/ionic-team/capacitor-plugins/issues/1366#issuecomment-1427893963

I think is actually along the correct lines of what the fix should be. The issue is that the web view is loaded before the map view component is even available, which confuses the JS code that executes after. Any subsequent calls into the Google Maps component will 100% yield a null reference exception because the map controller is simply not initialized. This completely screams lifecycle issue, to me, honestly.

Currently the code just invokes the resolve callback without any concern for if the map view controller is active or not. It just assumes so. The reason why the fix I linked above works is because it creates effectively a spinlock which ensures that the map view is always initialized before proceeding with invoking the resolve callback.

I'm honestly not sure how the Google Maps plugin shipped in 5.0 considering how easy it is to reproduce this bug.

gm112 avatar Jul 02 '23 15:07 gm112

any solution for this?

IbrahimElkhatib avatar Nov 17 '23 14:11 IbrahimElkhatib

I believe the latest alpha version of the plugin does address the delay requirement, but I haven't tested it:

MR:1638

To be honest - I had to switch to a JS version for mapping, since this plugin proved to be less useful in several cases that we needed - like annotations (aka popups), marker SVG images and marker animation. I had to use Leaflet (+MapBox for tiles), since GoogleMaps JS requires a realtime download to import their JS libraries, which is not something we wanted. It was also such a nuisance to setup and use - no Vue (or any JS framework) support at all - just bare-bone procedural JS - a loaded foot-gun on every corner.

avioli avatar Nov 19 '23 22:11 avioli

Still not a fix? I am tired of trying everyhting and couldn't make it load. Only the google logo is loading inside a gray area. :(

greg-md avatar Dec 11 '23 20:12 greg-md

I believe the latest alpha version of the plugin does address the delay requirement, but I haven't tested it:

MR:1638

To be honest - I had to switch to a JS version for mapping, since this plugin proved to be less useful in several cases that we needed - like annotations (aka popups), marker SVG images and marker animation. I had to use Leaflet (+MapBox for tiles), since GoogleMaps JS requires a realtime download to import their JS libraries, which is not something we wanted. It was also such a nuisance to setup and use - no Vue (or any JS framework) support at all - just bare-bone procedural JS - a loaded foot-gun on every corner.

This doesn't fix the issue - because the issue is that the native code seems to fire off before the page is fully ready to have the map container injected/overlayed, hence why the fix suggested by https://github.com/ionic-team/capacitor-plugins/issues/1366#issuecomment-1427893963 works so well.

Particularly this bit of code:

 else { // add this else case
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) {
        self.render(callback)
    }
    return;
}

Though, I do not believe this to be the actual fix as I believe this is merely pushing the chance of this bug happening to a condition that's pretty much never going to happen. I think the issue is that some of the code for the Map Controller just simply executes too early.

gm112 avatar Apr 21 '24 18:04 gm112

@judge I know this finding is some five months late, but to make my solution work in your context, since I use Vue and you do not - add a timeout before calling GoogleMap.create to give a chance to capacitor-google-map to connect properly.

I personally wasn't able to make it work with the styles suggested in the official "documentation" - I had to use a wrapper DIV, so in your case I would suggest you do:

this.innerHTML = `
  <style>
    #map-wrapper {
      display: inline-block;
      width: 200px;
      height: 400px;
      position: relative;
    }
    capacitor-google-map {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
    }
  </style>
  <div id="map-wrapper" style="border: 1px solid red;">
    <capacitor-google-map id="main-map"></capacitor-google-map>
  </div>
`;

I tested the above wrapper styles within my Vue app and it worked for me, but I had to move the red border to the wrapper - otherwise the dodgy logic in Maps.swift fails to identify the ScrollView :)

I think this was a good hint. I spent some time now debugging some issues of this plugin but I can also now create the map with this help. I used this styling for my map which did the fix for me. It worked without a wrapper. I noticed that height: 99.9vh did not work on some devices. I used e.g. an iPhone SE (1gen) with IOS 14, or IOS 15. Only if I change the height with a difference of 10vh of the fullheight, it worked. When I use height: 99vh it didn't work. No idea why... - maybe it depends on the device height.

capacitor-google-map {
  display: inline-block;
  width: 100vw;
  height: 115vh;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  position: fixed !important;
  z-index: 1;
}

A workaround might be to render the map with a styling of 200vh and when the map is initialised to reduce it again to 100vh.

EDIT:

I noticed that after page transitions, the map doesn't reappear anymore... (with the fix with the styling)

ngmiduc avatar May 27 '24 07:05 ngmiduc

@ngmiduc does it work with the fix with the styling, or with @gm112 suggestion using the spinlock, or both of them are needed?

alexp25 avatar Jun 11 '24 16:06 alexp25

@ngmiduc does it work with the fix with the styling, or with @gm112 suggestion using the spinlock, or both of them are needed?

I have tested the solution of @gm112 but I didn't resolve my issue. I added some extra logs and it seems to be an infinite loop where the reference to the google maps was lost by the ViewControler.

What works for me now is to set the width and height to 100vh and 100vw. I used to have it at 99.9vh, but this worked on some devices but not on other devices. There is also no way to change the height dynamically. There are apparently some specific styling dimensions for some specific devices that will make the app disappear and page transitions will retrigger the native map function onDisplay which could make the app disappear when the map has some bad heights or widths. That was at least my experience with the map.

ngmiduc avatar Jun 11 '24 16:06 ngmiduc

@ngmiduc It seems that using this component is required for iOS: <capacitor-google-map id="map"></capacitor-google-map> Before I was using a div, which worked fine on Android, and before that for some reason I tried using capacitor-google-maps instead of capacitor-google-map which didn't work. (Note I'm using Angular)

"The Google Maps Capacitor plugin ships with a web component that must be used to render the map in your application as it enables us to embed the native view more effectively on iOS" (from the readme) It looks like the component implements the style hack with the height.

I currently managed to get it shown, but only for a few seconds, until I'm starting to fill up the map, and for some reason it disappears (that might be related to something else in my project though)

alexp25 avatar Jun 11 '24 17:06 alexp25

I found out why the map disappeared. It seems to be related to dynamically adding a new element to the page where the map is created. Could someone explain why would that happen? Would the map binding break or how can this be avoided?

alexp25 avatar Jun 12 '24 15:06 alexp25

@yoaquim, I am also facing the exact same problem. Did you find any solution?

bravesoul349 avatar Aug 06 '24 09:08 bravesoul349