mapbox / mapbox/mapbox-maps-ios
TapInteraction inside Map content closure causes MetalView retention / memory growth when views are repeatedly created and destroyed
Nobody has claimed this yet.
- Dominant language
- Swift
- Stars
- 601
- Forks
- 196
- PR merge metrics
- No merged PRs in 30d
Description
## Environment
- Xcode version: 26.2
- iOS version: 26.3.1 (a)
- Devices affected: iPhone (directly, not simulator)
- Maps SDK Version: Mapbox Maps iOS 11.20.1 (also reproduced on 11.15.2)
- SwiftUI with NavigationStack (not UIKit)
- Custom GeoJSON clustering layers (not PointAnnotationGroup)
## Observed behavior and steps to reproduce
**Description**
I'm experiencing ever increasing memory when using TapInteraction inside a Map { } content closure in a detail view that is repeatedly pushed and popped via NavigationStack. MetalView instances are retained after back-navigation and never freed, eventually leading to iOS killing the app for excessive memory use.
Switching from TapInteraction (map content) to .onLayerTapGesture (view modifier) with identical closure contents resolves the issue.
**Setup**
I have a list of items, each of which pushes a detail view containing a Mapbox Map() with custom GeoJSON clustering layers (created programmatically via map.addSource / map.addLayer). The detail view is created fresh on each navigation push and cleaned up on pop (sources/layers removed, MapboxMap reference set to nil in .onDisappear).
**Reproduction**
- Push a detail view containing a Map() with a TapInteraction targeting a clustering layer from a list in a NavigationStack
- Tap back
- Repeat
- Observe in Xcode's Memory Graph Debugger that MetalView instance count grows with each visit and never decreases
- Memory climbs with each map loaded until iOS terminates the app after 10-12 of this forward-back interaction
**What I observed**
- An identical detail view without TapInteraction (for a different feature in my app) that follows the exact same Map() creation/teardown pattern does NOT exhibit this issue. Memory rises on entry, drops on back, stays stable across many visits.
- MapView instances are not accumulating — consistently 1 in the memory graph. MapViews are properly deallocated on back-navigation.
- MetalView instances though do accumulate — one is retained per detail view visit and never freed.
- Each retained MetalView is held alive by Core Animation infrastructure (CAMetalLayer, FPCAMemoryLayerState) and contains a reference to a "Closure context" in the memory graph.
**The code that causes the problem**
I'm using the approach shown in the [Interactions API documentation](https://docs.mapbox.com/ios/maps/guides/user-interaction/Interactions/) and the [SwiftUI Clustering example](https://docs.mapbox.com/ios/maps/examples/swiftui-clustering/) but to zoom into a cluster vs showing an alert:
```
MapReader { proxy in
Map(viewport: $viewport) {
// Handle cluster taps - only need biggest target, the background layer
TapInteraction(.layer("cluster-background")) { feature, context in
zoomToCluster(feature: feature)
return true // Stop propagation
}
}
...
}
private func zoomToCluster(feature: FeaturesetFeature) {
guard case let .point(p) = feature.geometry else { return }
guard let map = mapboxMap else { return }
let center = p.coordinates
let current = map.cameraState.zoom
let targetZoom = min(current + 3.0, 15.0)
withViewportAnimation(.easeOut(duration: 0.4)) {
self.viewport = .camera(
center: center,
zoom: targetZoom,
bearing: 0,
pitch: 0
)
}
}
```
## Expected behavior
Memory is released after map is cleaned up, like it is when using .onLayerTapGesture.
## Notes / analysis
I ran a number of tests to identify that a TapInteraction that specifically interacts with the map (like zooming) or captures self was causing the problem:
1. Identical map set up with no TapInteraction clause --> no memory issue
2. Identical map set up with a TapInteraction that only prints a log --> no memory issue
3. TapInteraction with self.someStateVar = value (any @State write, but no map access) --> memory issue occurs
4. TapInteraction that controls the map with zoomToCluster code as shown above --> memory issue occurs
5. .onLayerTapGesture with identical zoomToCluster code as shown above --> no memory issue
This is the .onLayerTapGesture code example that works fine:
```
MapReader { proxy in
Map(viewport: $viewport) {
// No TapInteraction here
}
.onLayerTapGesture("cluster-background") { queriedFeature, context in
zoomToCluster(center: context.coordinate)
return true
}
}
```
## Questions
Am I doing something wrong that is causing .TapInteraction to behave differently than .onLayerTapGesture? If so, I'm happy to be told what to do instead!
Perhaps TapInteraction vs .onLayerTapGesture are meant to be used for different cases? If so it would be helpful to have this documented. However, in the example with an alert showing on the page [SwiftUI Clustering Example](https://docs.mapbox.com/ios/maps/examples/swiftui-clustering/), I would imagine this will have the same problem as it refers to "self.details"... perhaps the issue doesn't manifest because the map isn't destroyed as a user navigates around.
Is there a known issue with TapInteraction closures that capture self preventing proper cleanup of Metal rendering resources when the Map view is destroyed? If so, it would b great to get that fixed!
## Attachments
Here is an example of my UI showing the navigation stack, the detail view with clustering, and the detail view after zooming into a cluster (all the dots).
Here are the Metal Views hanging around after hitting back from a map, as well as one of their memory graphs.
Here is my memory after going back and forth using TapInteraction vs .onLayerTapGesture, each about 9 times
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reproducing the NavigationStack push/pop cycle with Map and TapInteraction targeting the custom clustering layer, then compare it with the working onLayerTapGesture path using Xcode's Memory Graph Debugger. Trace the TapInteraction closure and Map teardown; done means MetalView instances are released after navigation back and repeated visits no longer cause memory growth.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- ios, swift
- Domain
- mobile, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100