mapbox-maps-ios
mapbox-maps-ios copied to clipboard
Updating an image in style - some features in SymbolLayer are not redrawn on current zoom level
Environment
- Xcode version: 15.0.1
- iOS version: 17.0.1
- Devices affected: iphone 15 pro max simulator and real device iphone 12 mini
- Maps SDK Version: MapboxMaps 11.1.0
Observed behavior and steps to reproduce
I’m writing a SwiftUI app using mapbox to display a number of pins. I’m using a SymbolLayer with FeatureCollection to display these pins. Each pin should display a remote URL image. But while remote images are being loaded I’m showing a standard pin image from local assets.
let imageID = "image\(event.id)"
try! map.addImage(pinImage, id: imageID)
feature.properties = ["image": JSONValue(imageID)]
/// and later in layer creation I set up the pin to use this image
pinLayer.iconImage = .expression(Exp(.get) { "image" })
Once any of the remotes are loaded I’m updating the image with pin’s id like this:
try! map.addImage(loadedImage, id: imageID)
Mapbox AI told me that this will replace the image with the new one and that all of the layers referencing this image should be redrawn to use this new picture. However what’s happening is that for current zoom level the old simple pin is still showing. If I zoom in/out new images are there, but for current zoom nothing changes. I tried force updating the map and the layer like this:
try? style.updateLayer(withId: pinLayerID, type: SymbolLayer.self) {_ in}
self.mapView.mapboxMap.triggerRepaint()
But nothing helps. Here is the whole relative code. the MapView creation:
class MapViewController: UIViewController {
var events: [Event]
internal var mapView: MapView!
var cancelables = Set<AnyCancelable>()
var map: MapboxMap {
mapView.mapboxMap
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func viewDidLoad() {
super.viewDidLoad()
if let token = Bundle.main.object(forInfoDictionaryKey: "MBXAccessToken") as? String {
let center = CLLocationCoordinate2D(latitude: 43.68563039075405, longitude: -79.30424301014425)
let cameraOptions = CameraOptions(center: center, zoom: 11)
MapboxOptions.accessToken = token
let myMapInitOptions = MapInitOptions(mapOptions: MapOptions(), cameraOptions: cameraOptions, styleURI: .dark)
mapView = MapView(frame: view.bounds, mapInitOptions: myMapInitOptions)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.view.addSubview(mapView)
mapView.mapboxMap.onMapLoaded.observeNext { _ in
// add simple local pins
self.addSymbolClusteringLayers()
}.store(in: &cancelables)
mapView.mapboxMap.onMapIdle.observeNext { _ in
// load remote images for all the pins and replace the old images with the new
self.loadRemotePinImages()
}.store(in: &cancelables)
}
}
}
pins creation:
extension MapViewController {
func addSymbolClusteringLayers() {
let pinImage = UIImage(named: "pin")!
var source = GeoJSONSource(id: sourceID)
var features: [Feature] = []
for event in self.events {
if let coord = event.coordinate {
var feature = Feature(geometry: Geometry.point(Point(LocationCoordinate2D(latitude: coord.latitude, longitude: coord.longitude))))
feature.identifier = FeatureIdentifier(event.id)
let imageID = "image\(event.id)"
// initially place simple pin image into map style for every pin
try! map.addImage(pinImage, id: imageID)
// setup image name to use for each feature - for now they are the same pinImage for every id, but we'll replace them later
feature.properties = ["image": JSONValue(imageID)]
features.append(feature)
}
}
let featureCollection = FeatureCollection(features: features)
let data = GeoJSONSourceData.featureCollection(featureCollection)
source.data = data
let pinLayer = SymbolLayer(id: pinLayerID, source: sourceID)
// take the image name we set up earlier. the image name will stay the same, but the image itself will be changed
pinLayer.iconImage = .expression(Exp(.get) { "image" })
try! map.addSource(source)
try! map.addLayer(pinLayer)
}
func loadRemotePinImages() {
Task.detached(priority: .background) {
await self.events.asyncForEach { event in
await self.loadRemotePinImage(event: event)
}
}
}
private func loadRemotePinImage(event: Event) async {
Task.detached(priority: .background) {
if let pinImage = await EventPinsCacheManager.shared.fetchPinImage(event: event) {
DispatchQueue.main.async {
// when the remote image is loaded - replace simple pin image with the new one for every image id. this should automatically update each feature in our symbol layer
try! self.map.addImage(pinImage, id: "image\(event.id)")
}
}
}
}
}
fetchPinImage basically does this:
let imageData = try? Data(contentsOf: imageUrl)
return UIImage(data: imageData)
and forms a nice pin image out of it
Expected behavior
I'd expect the pins on the map to be updated as soon as I call try! map.addImage(loadedImage, id: imageID)
for respective image
Notes / preliminary analysis
It really looks like a bug since it works for other zoom levels, it feels like I just need to call some force update mechanism and it will fix itself
Additional links and references
https://github.com/mapbox/mapbox-maps-ios/assets/9447630/22b88ff8-b480-4cad-bf12-c99e9f8bc585
https://github.com/mapbox/mapbox-maps-ios/issues/2093
Internal issue: https://mapbox.atlassian.net/browse/MAPSNAT-1768