-
Notifications
You must be signed in to change notification settings - Fork 24
Description
Description
Dragging nodes does not visually update on macOS 26.1 (Tahoe beta). The same code works correctly on macOS 15.7 (Sequoia).
Environment
- macOS version: 26.1 (Tahoe beta, build 25B5057f)
- Xcode version: Version 26.0 (17A324)
- Grape version: 1.1.0
- Swift version: 6.2
Symptoms
- ✅ Drag gesture IS detected (verified via print statements -
onDragChangefires,setNodeFixationis called) - ✅ Node positions ARE being updated internally (alpha jumps to 0.5, continuous position logs show coordinates changing)
- ❌ Canvas does NOT redraw to show the new node positions
- The simulation runs initially on appear, but dragging nodes shows no visual movement
Console Output
When dragging a node, we can confirm the gesture and fixation are working:
Started drag on node 17
Alpha before: 0.29947800323308066
Alpha after setFixation: 0.5
Dragging node 17 to (340.98046875, 152.203125)
Dragging node 17 to (347.15625, 154.1875)
Dragging node 17 to (360.171875, 160.7734375)
... (continues with position updates)
Released node 17
But the node never visually moves on screen.
Root Cause Analysis
The issue appears to be a regression in macOS 26 where SwiftUI Canvas doesn't redraw when observing @Observable properties.
In ForceDirectedGraph+View.swift, the Canvas relies on this pattern to trigger redraws:
Canvas { context, size in
let _ = model.currentFrame // This observation should trigger redraws
self.model.render(&context, size)
}On macOS 15.7, accessing model.currentFrame causes Canvas to redraw when currentFrame changes. On macOS 26, this observation pattern appears to be broken - the Canvas never invalidates.
Minimal Reproduction
Using the MyRing example from the Grape repo:
import Foundation
import Grape
import SwiftUI
import ForceSimulation
struct MyRingTest: View {
@State var graphStates = ForceDirectedGraphState(
initialIsRunning: true,
ticksOnAppear: .iteration(0)
)
static let strokeStyle = StrokeStyle(lineWidth: 1.5, lineCap: .round, lineJoin: .round)
var body: some View {
ForceDirectedGraph(states: graphStates) {
Series(0..<20) { i in
NodeMark(id: 3 * i + 0)
.symbolSize(radius: 20.0)
.foregroundStyle(.green)
NodeMark(id: 3 * i + 1)
.symbol(.pentagon)
.symbolSize(radius: 24)
.foregroundStyle(.blue)
NodeMark(id: 3 * i + 2)
.symbol(.circle)
.symbolSize(radius: 20.0)
.foregroundStyle(.yellow)
LinkMark(from: 3 * i + 0, to: 3 * i + 1)
LinkMark(from: 3 * i + 1, to: 3 * i + 2)
LinkMark(from: 3 * i + 0, to: 3 * ((i + 1) % 20) + 0)
LinkMark(from: 3 * i + 1, to: 3 * ((i + 1) % 20) + 1)
LinkMark(from: 3 * i + 2, to: 3 * ((i + 1) % 20) + 2)
}
.stroke(.secondary, Self.strokeStyle)
} force: {
.manyBody(strength: -30)
.link(
originalLength: 50.0,
stiffness: .weightedByDegree { _, _ in 1.0 }
)
.center()
}
.graphOverlay { proxy in
Color.clear.contentShape(Rectangle())
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .local)
.onChanged { value in
if let nodeID = proxy.node(of: Int.self, at: value.startLocation) {
proxy.setNodeFixation(nodeID: nodeID, fixation: value.location, minimumAlpha: 0.8)
proxy.kineticAlpha = max(proxy.kineticAlpha, 0.5)
}
}
.onEnded { value in
if let nodeID = proxy.node(of: Int.self, at: value.startLocation) {
proxy.setNodeFixation(nodeID: nodeID, fixation: nil)
}
}
)
.withGraphMagnifyGesture(proxy)
}
}
}Results:
- ✅ Works on macOS 15.7 (Sequoia)
- ❌ Fails on macOS 26.1 (Tahoe)
Attempted Workarounds
1. TimelineView wrapper
Wrapping Canvas in TimelineView(.animation(...)) to force continuous redraws:
TimelineView(.animation(paused: !model.stateMixinRef.isRunning)) { timeline in
Canvas { context, size in
let _ = model.currentFrame
let _ = timeline.date // Force dependency on timeline
self.model.render(&context, size)
}
}Result: Did not resolve the issue.
2. Removing state-dependent styling
Ensured no conditional styling in ForceDirectedGraph content that could cause view recreation ("Graph state revived" messages).
Result: Eliminated revive spam but did not fix drag.
3. Manual setNodeFixation calls
Implemented custom drag gesture that directly calls proxy.setNodeFixation() and proxy.kineticAlpha = 0.5.
Result: Fixation IS being set (verified via logs), but Canvas doesn't redraw.
4. Local @State ownership
Moving ForceDirectedGraphState to be locally owned by the view containing ForceDirectedGraph.
Result: Did not help.
Possible Fixes
This may require Apple to fix the Canvas + Observation regression in macOS 26. Potential workarounds to explore:
- CADisplayLink - Manual frame updates outside of SwiftUI's observation system
- onChange with @State counter - Force view invalidation via explicit state mutation
- Metal/Core Graphics rendering - Bypass SwiftUI Canvas entirely
- File Apple Feedback - Report the SwiftUI Canvas + @observable regression
Additional Notes
The simulation DOES run and animate on initial appear - it's only the drag interaction that fails to trigger visual updates. This suggests the issue is specifically with how Canvas observes changes triggered by gesture handlers, not with the observation system in general.