Skip to content

Layer in GeoJSONSource occasionally visibly flickers away from target location #2367

@naftalibeder

Description

@naftalibeder

Environment

  • Xcode version: 26.0 (17A324)
  • iOS version: 26.0
  • Devices affected: Simulator, iPhone 12 mini
  • Maps SDK Version: 11.16.0

Observed behavior and steps to reproduce

  1. Place the component below in a new project:
ViewController.swift
import UIKit
import MapboxMaps

class ViewController: UIViewController {
  private var startDistance: Double = 0
  private var currrentDistance: Double = 0
  private var targetDistance: Double = 0
  private var startTimestamp: CFTimeInterval = 0
  private var displayLink: CADisplayLink? {
    didSet { oldValue?.invalidate() }
  }
  private var cancelables = Set<AnyCancelable>()
  
  private var mapView: MapView!
  private var mapCenterAnimator: BasicCameraAnimator?
  private var mapPanAnimator: BasicCameraAnimator?
  
  private let line = LineString(
    [
      .init(latitude: 42.36360, longitude: -71.05085),
      .init(latitude: 41.81250, longitude: -71.42216),
    ]
  )
  private let duration: Double = 1
  
  deinit {
    displayLink?.invalidate()
  }

  override func viewDidLoad() {
    super.viewDidLoad()

    mapView = MapView(frame: view.bounds, mapInitOptions: .init())
    mapView.mapboxMap.styleURI = .standard
    
    mapView.mapboxMap.onStyleLoaded.observeNext { _ in
      var source = GeoJSONSource(id: "source-point")
      source.data = .geometry(.point(.init(self.line.coordinates[0])))
      try! self.mapView.mapboxMap.addSource(source)
      
      var circleOuterLayer = CircleLayer(id: "layer-circle-outer", source: "source-point")
      circleOuterLayer.circleRadius = .constant(15)
      circleOuterLayer.circleColor = .constant(.init(hue: 1, saturation: 1, lightness: 1)!)
      try! self.mapView.mapboxMap.addLayer(circleOuterLayer)
      
      var circleInnerLayer = CircleLayer(id: "layer-circle-inner", source: "source-point")
      circleInnerLayer.circleRadius = .constant(12)
      circleInnerLayer.circleColor = .constant(.init(hue: 0.7, saturation: 0.8, lightness: 0.5)!)
      try! self.mapView.mapboxMap.addLayer(circleInnerLayer)
    }.store(in: &cancelables)
    
    self.view.addSubview(mapView)
    
    Timer.scheduledTimer(withTimeInterval: duration, repeats: true) { _ in
      self.move()
    }
  }
  
  private func move() {
    startTimestamp = CACurrentMediaTime()
    startDistance = currrentDistance
    targetDistance += 60
    let currentPoint = line.coordinateFromStart(distance: targetDistance)!
    
    let bounds = CoordinateBounds(
      southwest: currentPoint.coordinate(at: 200, facing: 45),
      northeast: currentPoint.coordinate(at: 200, facing: 180 + 45),
    )
    
    var cameraOptions: CameraOptions
    do {
      cameraOptions = try mapView.mapboxMap.camera(
        for: [bounds.northeast, bounds.southwest],
        camera: .init(
          padding: .zero,
          bearing: currentPoint.direction(to: line.coordinateFromStart(distance: targetDistance + 100)!),
          pitch: 60
        ),
        coordinatesPadding: nil,
        maxZoom: nil,
        offset: nil
      )
    } catch {
      print("MapView | Error setting camera: \(error)")
      return
    }
    
    mapCenterAnimator?.stopAnimation()
    mapCenterAnimator = mapView.camera.makeAnimator(duration: duration, curve: .linear) { transition in
      transition.center.toValue = bounds.center
    }
    mapCenterAnimator!.startAnimation()
    
    mapPanAnimator?.stopAnimation()
    mapPanAnimator = mapView.camera.makeAnimator(duration: duration, curve: .linear) { transition in
      transition.zoom.toValue = cameraOptions.zoom
      transition.bearing.toValue = cameraOptions.bearing
      transition.pitch.toValue = cameraOptions.pitch
    }
    mapPanAnimator!.startAnimation()
    
    displayLink = CADisplayLink(target: self, selector: #selector(updateFromDisplayLink(displayLink:)))
    displayLink?.add(to: .current, forMode: .common)
  }
  
  @objc private func updateFromDisplayLink(displayLink: CADisplayLink) {
    let progressRatio = (CACurrentMediaTime() - startTimestamp) / duration
    
    guard progressRatio <= 1 else {
      displayLink.invalidate()
      self.displayLink = nil
      return
    }
    
    currrentDistance = startDistance + (targetDistance - startDistance) * progressRatio
    let point = line.coordinateFromStart(distance: currrentDistance)!
    mapView.mapboxMap.updateGeoJSONSource(
      withId: "source-point",
      geoJSON: .geometry(.point(.init(point)))
    )
  }
}
  1. Build and run the project.

Expected behavior

The dot should smoothly slide along with the map.

Notes / preliminary analysis

Instead, the dot occasionally flickers off of its intended coordinates. See video, or an example normal/bad frame comparison:

Image Image

It appears that the "bad" frame shows the normal layer joined with a slightly offset version of the layer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug 🪲Something is broken!

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions