Skip to content

feat(ios): render the map into a flutter texture instead of a platform view - #1143

Open
Nixxx19 wants to merge 2 commits into
mapbox:mainfrom
Nixxx19:feat/ios-texture-mode
Open

feat(ios): render the map into a flutter texture instead of a platform view#1143
Nixxx19 wants to merge 2 commits into
mapbox:mainfrom
Nixxx19:feat/ios-texture-mode

Conversation

@Nixxx19

@Nixxx19 Nixxx19 commented Aug 28, 2026

Copy link
Copy Markdown

closes #1142

on ios the map is a UiKitView, so uikit composites it and it is not in flutter's scene. nothing can read it back: toImageSync and RepaintBoundary.toImage return a hole where the map is, and a custom ImageFilter.shader used as a backdrop samples an empty texture. android avoids this by rendering into a TextureView; ios has no equivalent because its platform views are always uikit composited (flutter/flutter#43902).

this adds a texture path for ios. the map is created without a platform view and its frames go to FlutterTextureRegistry, so the widget tree contains a Texture and nothing else.

MapTexture(
  styleUri: MapboxStyles.MAPBOX_STREETS,
  onMapCreated: (map) async {
    await map.setCamera(...);
    await map.style.addLayer(...);
  },
)

what it gives you

  • the map can be captured. screenshots, thumbnails and share cards that include the map now work.
  • custom shaders and backdrop filters can sample the map, so glass, scrims and blurs driven by map luminance behave like they do anywhere else.
  • the map composites like any other widget: transforms, opacity, clips, widgets between layers.
  • it is faster than the platform view on the numbers below.
  • everything already in the plugin keeps working through it: the full MapboxMap api, style, camera, sources, layers, view annotations, and the logo and attribution.

before and after

same page, same button, same code, both on an iphone. the panel on the right is the result of RepaintBoundary.toImage on the page.

MapWidget (platform view) MapTexture
the capture is blank. the map is not in flutter's scene, so there is nothing to capture. the capture contains the map: streets, labels, scale bar, logo and attribution.

performance

same device, same camera path, profile mode, frame timings from SchedulerBinding.addTimingsCallback. a small widget repaints continuously during the run so flutter produces frames to measure.

MapWidget (platform view) MapTexture
frames 136 128
build p50 0.35 ms 0.31 ms
build p90 0.43 ms 0.40 ms
raster p50 0.74 ms 0.56 ms
raster p90 0.96 ms 0.70 ms

raster p90 is 27% lower with the texture. ios platform view compositing splits the scene into overlay layers around the view, and a texture is one more layer in a single scene, so the per frame copy costs less than the compositing it replaces. the example app ships the page that produces these numbers, so the result is reproducible rather than quoted.

how it works

HeadlessMapTexture builds the ordinary MapboxMapController with a channel suffix and parks its view offscreen inside the app's own key window, so every pigeon api behaves exactly as it does for the platform view. MapTexturePublisher copies each finished frame into an IOSurface backed CVPixelBuffer and hands it to the texture registry. on the dart side MapTexture sizes the map to its constraints, forwards gestures and disposes the native map when it unmounts.

three details worth calling out, because none of them is obvious:

frames are caught at -[MTLCommandBuffer presentDrawable:]. UIView.draw(_:) looks like the hook and never runs, because an MTKView renders through metal rather than core graphics. presentDrawable also lets the copy be encoded onto the same command buffer that presents it, so it is ordered after the render with no waiting.

the offscreen map lives in the app's existing key window. a second UIWindow takes over the scene. staying composited is what keeps coreanimation recycling drawables.

the logo and attribution are rasterised and drawn by flutter. they are uikit views drawn with core graphics, so they are not in the metal output. attribution is preserved.

gestures

pan, pinch, rotate and pitch are forwarded and drive the camera directly, pan through dragCameraOptions which is what the sdk's own recogniser uses.

scope

MapWidget is untouched. MapTexture is additive, ios only and opt in. the 20 lines changed in existing files are an export, a part, a MapboxMap.headless factory and a getter on MapboxMapController so it can be hosted outside a platform view. nothing existing changes behaviour.

tests and verification

  • 8 widget tests in test/map_texture_test.dart: creation at the size of its constraints, the texture rendering once the host returns an id, a unique channel suffix per map, style pass through, resize on a constraint change, disposal of the native map when it leaves the tree, gesture forwarding, and nothing forwarded when gestures are off
  • flutter analyze clean on every touched file
  • two example pages: the capture comparison and the performance comparison
  • verified on an iphone: capture, annotations, pan, pinch, rotate, pitch, resize and disposal

follow ups

happy to take these in this pr or later, whichever you prefer:

  • the frame copy is a gpu blit. rendering straight into the flutter buffer is the obvious next step and would remove it.
  • fling momentum on pan.

@Nixxx19
Nixxx19 requested a review from a team as a code owner August 28, 2026 10:56
@Nixxx19
Nixxx19 requested a review from maios August 28, 2026 10:56
@CLAassistant

CLAassistant commented Aug 28, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@ox-security

ox-security Bot commented Aug 28, 2026

Copy link
Copy Markdown

OX Security Logo

Successfully scanned changes introduced in a pull request into main from feat/ios-texture-mode.

Internal scan identifier: 5af9cc92-a9b4-4385-bea0-fd78d7392301.

Total issues Blocking issues Scan status
1 0 ✔️
Category Issues
Open Source Security 1

See all issues found during this scan in the OX Security Application.

Detailed information
Issue #1
Namekotlin-gradle-plugin@2.1.0 • 1 direct CVE • EPSS Low
StatusNew
EnforcementMonitor
SeverityHigh
CategoryOpen Source Security
Source toolsOX Open Source Security
Recommendation• Current Dependency: kotlin-gradle-plugin@2.1.0
• The latest version of this package (2.2.0) has the same (or more) number of vulnerabilities as the current version
• You should reconsider the use of this package
1 aggregation
FileMatch
android/build.gradleorg.jetbrains.kotlin:kotlin-gradle-plugin@2.1.0

@Nixxx19
Nixxx19 marked this pull request as draft August 28, 2026 10:58
@Nixxx19
Nixxx19 force-pushed the feat/ios-texture-mode branch from 7454b07 to cfd9176 Compare August 28, 2026 11:01
@Nixxx19
Nixxx19 marked this pull request as ready for review August 28, 2026 11:01
@luizvnegrini

Copy link
Copy Markdown

awesome

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.

iOS: the map cannot be captured or read by flutter, because it is a platform view

3 participants