Skip to content

[Bug]: HeatmapLayer is missing the LayerPropsCommon codepart, so on iOS it never receives sourceLayer or filter #4268

Description

@bjornhansen

Mapbox Version

11.15.2

React Native Version

0.81.5

Platform

iOS

@rnmapbox/maps version

10.2.6

Standalone component to reproduce

I have not reproduced this at runtime. Per .github/REPRODUCING.md rule 5 I'm saying so up front, and flagging the consequence below as inferred rather than observed. What I did verify is a structural difference in this repo's own source, which should be quick to confirm.

The shape it would take — a heatmap over a vector tile source:

import React from 'react';
import { View } from 'react-native';
import Mapbox from '@rnmapbox/maps';

const TILES = 'https://example.com/tiles/{z}/{x}/{y}.pbf';

export default function BugReportExample() {
  return (
    <View style={{ flex: 1 }}>
      <Mapbox.MapView style={{ flex: 1 }}>
        <Mapbox.Camera zoomLevel={10} centerCoordinate={[-81.69, 41.5]} />
        <Mapbox.VectorSource id="tileset" tileUrlTemplates={[TILES]}>
          <Mapbox.HeatmapLayer
            id="heat"
            sourceLayerID="my_points"
            filter={['==', ['get', 'kind'], 'a']}
            style={{ heatmapRadius: 30, heatmapOpacity: 1 }}
          />
        </Mapbox.VectorSource>
      </Mapbox.MapView>
    </View>
  );
}

Neither sourceLayerID nor filter reaches the native layer on iOS — see below for why.
Swapping HeatmapLayer for CircleLayer, everything else equal, should render.

Expected: the heatmap renders from my_points, filtered.
Inferred actual on iOS: nothing renders, because the layer has no source-layer to read. A ShapeSource should be unaffected, since source is set by the constructor and sourceLayer is meaningless there.

Observed behavior and steps to reproduce

RNMBXHeatmapLayer.swift is the only vector-capable layer without the @{codepart-replace-start(LayerPropsCommon.codepart-swift.ejs, ...)} block. Checked against main today:

Layer codepart marker
Circle present
Fill present
Line present
Symbol present
Raster present
FillExtrusion present
Heatmap absent

That block is what assigns sourceLayer, source and filter under RNMBX_11. Its absence matters because RNMBXLayer.setOptions has its entire body inside #if !RNMBX_11:

func setOptions(_ layer: inout Layer) {
    setBaseOptions(&layer)
    #if !RNMBX_11
    if let sourceLayerID = sourceLayerID { layer.sourceLayer = sourceLayerID }
    ...
    if let filter = filter, filter.count > 0 { ... }
    #endif
}

Since the podspec always compiles -D RNMBX_11, that is a no-op in every current build. RNMBXHeatmapLayer.makeLayer calls exactly that no-op and nothing else:

override func makeLayer(style: Style) throws -> Layer {
    let _ : VectorSource = try self.layerWithSourceID(in: style)
    #if RNMBX_11
    var layer: Layer = LayerType(id: self.id!, source: self.sourceID!)
    #else
    var layer: Layer = LayerType(id: self.id!)
    #endif
    setOptions(&layer)
    return layer
}

By contrast RNMBXCircleLayer.makeLayer assigns layer.sourceLayer directly and carries the codepart. So a heatmap ends up with source (from the constructor) but never source-layer and never filter.

Two consequences, the second arguably worse than the first:

  1. On a VectorSource the layer has no source-layer to read, so it should draw nothing.
  2. filter is never applied at all — so filtering a heatmap would silently do nothing rather than fail visibly.

Android looks unaffected: RNMBXHeatmapLayer.kt calls layer.sourceLayer(mSourceLayerID!!) directly.

This may explain some older iOS-side "heatmap doesn't show" reports, though the ones I found (#3474, #3163) are Android crashes and look unrelated.

Expected behavior

HeatmapLayer honours sourceLayerID and filter on iOS, as the other layer types do.

Notes / preliminary analysis

The fix appears to be adding the codepart marker to RNMBXHeatmapLayer.swift and running yarn generate. As I read scripts/codepart-replace.mjs, generation scans for the marker rather than working from a manifest, so it can't introduce a missing block on its own — the marker has to be added to the file once.

Instantiating the existing template with {layerType:"Heatmap"} produces a block that diffs identically against RNMBXCircleLayer's once the type name is normalised, which is the change we're carrying locally as a patch-package patch. Happy to open a PR with it, but I'd rather not send a fix I haven't verified against a reproducer — that seems to be exactly what REPRODUCING.md is asking contributors not to do. We expect to have this on a device shortly; if it behaves as described I can follow up with before/after evidence, or you may well prefer to just make the one-line change yourselves.

Caveats, stated plainly:

  • Everything above is from reading source, not running it. The cause (missing marker, no-op setOptions) is verified against your source; the effect (blank layer, ignored filter) is inferred.
  • I did not build the /example app. It needs an Xcode/pods setup and Mapbox credentials that this environment doesn't have, and the finding is structural rather than timing-dependent, so a reproducer seemed unlikely to change the diagnosis. I'd have run it otherwise.
  • I have not checked whether RasterParticleLayer, HillshadeLayer or the newer layer types have the same gap.

Additional links and references

  • ios/RNMBX/codeparts/LayerPropsCommon.codepart-swift.ejs — the template
  • ios/RNMBX/RNMBXCircleLayer.swift — a layer that has the block
  • ios/RNMBX/RNMBXHeatmapLayer.swift — the one that doesn't
  • scripts/codepart-replace.mjs — marker-driven generation

Thanks for .github/REPRODUCING.md, by the way — the #4252/#4253 example is a sharp illustration, and it's the reason this is an issue and not a speculative PR.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug 🪲Something isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions