MeshMap performance: quick wins (#1490)

* MeshMap: change onMapCameraChange frequency to .onEnd so that zooming doesn't cause continuous SwiftUI reevaluation on every frame

* MeshMapContent: factor out reducedPrecisionMapCircles into a separate function

* MeshMapContent: when multiple reducedPrecisionCircles have the same (lat,lon,radius), just draw one (big perf boost in dense areas)
This commit is contained in:
Mike Robbins 2025-10-30 17:15:18 -04:00 committed by GitHub
parent 59d106ac1e
commit 8df71404b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 51 additions and 13 deletions

View file

@ -15,6 +15,12 @@ struct IdentifiableOverlay: Identifiable {
var id: ObjectIdentifier { ObjectIdentifier(overlay as AnyObject) }
}
struct ReducedPrecisionMapCircleKey: Hashable {
let latitudeI: Int32
let longitudeI: Int32
let precisionBits: Int32
}
struct MeshMapContent: MapContent {
/// Parameters
@ -43,7 +49,7 @@ struct MeshMapContent: MapContent {
@FetchRequest(sortDescriptors: [NSSortDescriptor(key: "name", ascending: true)],
predicate: NSPredicate(format: "enabled == true", ""), animation: .none)
private var routes: FetchedResults<RouteEntity>
@MapContentBuilder
var positionAnnotations: some MapContent {
ForEach(positions, id: \.id) { position in
@ -60,16 +66,7 @@ struct MeshMapContent: MapContent {
let nodeColor = UIColor(hex: UInt32(position.nodePosition?.num ?? 0))
let positionName = position.nodePosition?.user?.longName ?? "?"
/// Reduced Precision Map Circle
if 12...15 ~= position.precisionBits {
let pp = PositionPrecision(rawValue: Int(position.precisionBits))
let radius: CLLocationDistance = pp?.precisionMeters ?? 0
if radius > 0.0 {
MapCircle(center: position.coordinate, radius: radius)
.foregroundStyle(Color(nodeColor).opacity(0.25))
.stroke(.white, lineWidth: 1)
}
}
// Use a hash of the position ID to stagger animation delays for each node, preventing synchronized animations and improving visual distinction.
let calculatedDelay = Double(position.id.hashValue % 100) / 100.0 * 0.5
@ -91,7 +88,46 @@ struct MeshMapContent: MapContent {
}
}
}
private var reducedPrecisionCircleItems: [(nodeNum: Int64, circleKey: ReducedPrecisionMapCircleKey)] {
// Precompute *unique* reduced-precision circles so we don't have to redraw tons of identical (center, radius) circles in dense map areas. (Since they're all transparent, this causes severe FPS drop when zoomed into areas where there are a ton of overlapping circles.)
var lowestNumForKey: [ReducedPrecisionMapCircleKey: Int64] = [:]
// Populate a dict where the key is (lat, lon, bits) and the value is the *lowest* node.num seen for that key.
// That lowest node.num value is used to create a stable color for the MapCircle and stable id for ForEach.
for position in positions {
// Same filter criteria as positionAnnotations:
if (!showFavorites || (position.nodePosition?.favorite == true)) && !(position.nodePosition?.ignored == true) {
if 12...15 ~= position.precisionBits {
let nodeNum = position.nodePosition?.num ?? 0
let key = ReducedPrecisionMapCircleKey(latitudeI: position.latitudeI, longitudeI: position.longitudeI, precisionBits: position.precisionBits)
if let existing = lowestNumForKey[key] {
if nodeNum < existing { lowestNumForKey[key] = nodeNum }
} else {
lowestNumForKey[key] = nodeNum
}
}
}
}
// Sort by nodeNum just to keep draw order stable.
return lowestNumForKey.map { ($0.value, $0.key) }.sorted { $0.nodeNum < $1.nodeNum }
}
@MapContentBuilder
var reducedPrecisionMapCircles: some MapContent {
ForEach(reducedPrecisionCircleItems, id: \.nodeNum) { item in
let circleKey = item.circleKey
let nodeNum = item.nodeNum
let radius = PositionPrecision(rawValue: Int(circleKey.precisionBits))?.precisionMeters ?? 0
if radius > 0.0 {
let center = CLLocationCoordinate2D(latitude: Double(circleKey.latitudeI) / 1e7, longitude: Double(circleKey.longitudeI) / 1e7)
let nodeColor = UIColor(hex: UInt32(nodeNum))
MapCircle(center: center, radius: radius)
.foregroundStyle(Color(nodeColor).opacity(0.25))
.stroke(.white, lineWidth: 1)
}
}
}
@MapContentBuilder
var routeAnnotations: some MapContent {
ForEach(routes) { route in
@ -167,6 +203,7 @@ struct MeshMapContent: MapContent {
}
positionAnnotations
reducedPrecisionMapCircles
routeAnnotations
waypointAnnotations
}

View file

@ -75,7 +75,8 @@ struct MeshMap: View {
}
.controlSize(.regular)
.offset(y: 100)
.onMapCameraChange(frequency: MapCameraUpdateFrequency.continuous, { context in
.onMapCameraChange(frequency: MapCameraUpdateFrequency.onEnd, { context in
// distance is only used for long-press waypoint creation, so we don't need continuous updates which touch @State and force rerenders as we pan and (for distance in particular) zoom around the map. onEnd is more than enough.
distance = context.camera.distance
})
.onTapGesture(count: 1, perform: { position in