Skip to content

fix(ios): load the map view when no container matches - #179

Open
adam-enzy wants to merge 1 commit into
ionic-team:mainfrom
adam-enzy:fix/ios-map-view-never-loads-without-container
Open

adam-enzy wants to merge 1 commit into
ionic-team:mainfrom
adam-enzy:fix/ios-map-view-never-loads-without-container

Conversation

@adam-enzy

Copy link
Copy Markdown

Fixes the long-standing iOS Unexpectedly found nil while implicitly unwrapping an Optional value crash: #129, #131, #132, #135.

The bug

GMapView is assigned in exactly one place — GMViewController.viewDidLoad() — which only runs when the view controller's view is first touched. The only touch is inside the if let target branch of render():

self.targetViewController = self.getTargetContainer(refWidth: ..., refHeight: ...)

if let target = self.targetViewController {
    target.tag = Map.MAP_TAG
    target.removeAllSubview()
    self.mapViewController.view.frame = target.bounds   // ← the only thing that loads the view
    target.addSubview(self.mapViewController.view)
}

getTargetContainer() looks for a WKChildScrollView/WKScrollView whose contentSize is an exact floating-point match for the element's reported size:

let isWidthEqual  = width == refWidth
let isHeightEqual = actualHeightFloor == refHeight || actualHeightCeil == refHeight

That misses whenever the page is still laying out — most commonly when the map is created while a page transition is animating. When it misses, the branch is skipped, viewDidLoad never runs, and GMapView stays nil for the lifetime of the map.

Nothing recovers from that state:

  • onViewDidLoad never fires, so finishMapConfiguration() never runs and onMapReady is never emitted — so JS has no way to detect the failure either.
  • Every subsequent call from JS (setCamera, enableCurrentLocation, addMarkers, …) dereferences the nil implicitly-unwrapped optional and hard-crashes the app with EXC_BREAKPOINT / SIGTRAP.

Symbolicated from a production crash (@capacitor/google-maps 8.0.1, iOS 26.6, iPhone 15):

Exception: EXC_BREAKPOINT (SIGTRAP), brk 1
Thread 0 CRASHED (com.apple.main-thread):
  0  App  closure #1 in Map.enableCurrentLocation(enabled:)
  3  libdispatch  _dispatch_client_callout
  4  libdispatch  _dispatch_async_and_wait_invoke
Thread 2 (queue "bridge"), blocked on thread 0:
  6  Capacitor  closure #2 in CapacitorBridge.handleJSCall(call:)

Registers confirm it: the mapViewController pointer is valid, GMapView is 0. The instruction stream is ldr x8,[x0,#192] → ldr x0,[x8,x9] → cbz x0 → brk #1.

Relationship to #136

#136 fixed this crash for the plugin's own configuration by moving GMapView.delegate / styles / zoom out of render() and deferring them to the new onViewDidLoad hook. That was the right mechanism, but it left the view load itself inside the if let target branch — so when no container matches, onViewDidLoad never fires and every caller-facing method is still exposed to the same nil. That's consistent with #129/#131/#132/#135 still being open.

This change is the remaining step of that fix.

The change

When no container matched, load the view anyway:

} else {
    self.mapViewController.loadViewIfNeeded()
}

viewDidLoad runs, GMapView is assigned, onViewDidLoad fires, finishMapConfiguration() runs and onMapReady is emitted as normal. The view isn't attached to a container yet, but rebindTargetContainer() already handles that on the next onResize/onDisplay pass — which is the existing recovery path, now reachable instead of dead.

Net effect: a missed container lookup degrades to a map that attaches a moment later instead of a process that crashes on the next call.

Notes

  • Behaviour is unchanged on the happy path — this only adds an else.
  • I don't have SwiftLint locally (macOS box, not installed), so I've kept to the surrounding style and am relying on CI lint.

GMapView is only assigned in GMViewController.viewDidLoad, which only runs
when the view is first touched -- and the only touch is inside the
`if let target` branch of render(). When getTargetContainer() finds no match,
that branch is skipped and GMapView stays nil for the lifetime of the map.

getTargetContainer() matches a WKWebView sub-scroll view whose contentSize is
an exact match for the element, so it misses whenever the page is still laying
out (e.g. while a page transition is animating). Nothing recovers from that:
onViewDidLoad never fires, so finishMapConfiguration() never runs and
onMapReady is never emitted, and every later call from JS -- setCamera,
enableCurrentLocation, addMarkers -- dereferences the nil implicitly-unwrapped
optional and hard-crashes the app with EXC_BREAKPOINT.

ionic-team#136 fixed this for the plugin's own configuration by deferring it to
onViewDidLoad, but left the view load itself inside the `if let`, so
caller-facing methods are still exposed. Call loadViewIfNeeded() when no
container matched so GMapView always exists; rebindTargetContainer() already
attaches the view on the next onResize/onDisplay pass.
@adam-enzy

Copy link
Copy Markdown
Author

For context, we're currently applying a patch in our build pipeline to fix this issue. Thought it would be helpful for everyone and we would love to stop having to patch it manually for ourselves.

Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant