ionic-team / capacitor-plugins

Official plugins for Capacitor ⚡️
531 stars 596 forks source link

Google Maps not loading on iOS (physical device) #1366

Open judge opened 1 year ago

judge commented 1 year ago

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
Ionitron commented 1 year ago

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 💙

DwieDima commented 1 year ago

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

judge commented 1 year ago

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

naqeeb-klabs commented 1 year ago

Hi @judge ,

Did you found any work around for this issue?

judge commented 1 year ago

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

akeeee commented 1 year ago

Hi @judge ,

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

judge commented 1 year ago

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.

Simon54 commented 1 year ago

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.

yoaquim commented 1 year ago

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 commented 1 year ago

@judge where you able to find a solution?

avioli commented 1 year ago

@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.

metinjakupi commented 1 year ago

Bump!

avioli commented 1 year ago

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:

That's it form me and good luck.

avioli commented 1 year ago

@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 :)

metinjakupi commented 1 year ago

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?

avioli commented 1 year ago

@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.

gm112 commented 1 year ago

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.

IbrahimElkhatib commented 1 year ago

any solution for this?

avioli commented 1 year ago

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.

greg-md commented 11 months ago

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. :(

gm112 commented 7 months ago

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.

ngmiduc commented 6 months ago

@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)

alexp25 commented 5 months ago

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

ngmiduc commented 5 months ago

@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.

alexp25 commented 5 months ago

@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 commented 5 months ago

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?

bravesoul349 commented 3 months ago

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

gm112 commented 3 weeks ago

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

The spinlock issue I noted will propagate as null exceptions coming from the native plugin, which you would visually see as the Google Maps component not rendering at all. So regarding the styling fix, I believe people are having another issue that is unrelated. I hope that clears things up.