SwiftFlowInstance
Programmatic controller for the canvas viewport and graph state queries. SwiftFlowInstance is an ObservableObject that provides imperative control over the viewport, coordinate conversion, and bounds calculation.
Basic Setup
@StateObject var instance = SwiftFlowInstance()
SwiftFlow(
nodes: nodes, edges: edges,
swiftFlowInstance: instance, ...
) { node in ... }While the instance is attached to a canvas, canvas state is mirrored into it (viewport, viewSize, nodeSizes), and viewport changes made through the instance — setViewport, fitView, setCenter, zoomTo, reset — are applied to the canvas immediately, even outside animations. deleteElements is routed through the canvas and honors its onBeforeDelete/onDelete behavior.
Published Properties
| Property | Type | Description |
|---|---|---|
viewport | Viewport | Current viewport (pan + zoom) state |
viewSize | CGSize | Size of the canvas view in points |
nodeSizes | [String: CGSize] | Map of node ID to measured dimensions |
Viewport Control
getViewport()
public func getViewport() -> ViewportReturns the current viewport state.
setViewport(_:animated:)
public func setViewport(_ vp: Viewport, animated: Bool = true)Sets the viewport directly. When animated is true, transitions with a 0.3s ease-in-out animation.
instance.setViewport(Viewport(x: -100, y: -50, zoom: 1.5))
instance.setViewport(Viewport(x: 0, y: 0, zoom: 1), animated: false)fitView(nodes:nodeSizes:options:)
public func fitView<T: Equatable & Sendable>(
nodes: [Node<T>],
nodeSizes: [String: CGSize],
options: FitViewOptions = FitViewOptions()
)Adjusts the viewport to frame all (or specific) nodes. Uses FitViewOptions for configuration. Parent-relative child positions are resolved, and unmeasured nodes use their explicit width/height or 150×50. The fitted content is centered in the viewport, including when the computed zoom is capped by maxZoom.
// Fit all visible nodes
instance.fitView(nodes: nodes, nodeSizes: instance.nodeSizes)
// Fit specific nodes with options
instance.fitView(
nodes: nodes,
nodeSizes: instance.nodeSizes,
options: FitViewOptions(padding: 100, maxZoom: 2.0, duration: 0.8)
)setCenter(x:y:zoom:animated:)
public func setCenter(x: CGFloat, y: CGFloat, zoom: CGFloat? = nil, animated: Bool = true)Centers the viewport on a canvas coordinate. If zoom is nil, preserves the current zoom level.
instance.setCenter(x: 250, y: 150)
instance.setCenter(x: 250, y: 150, zoom: 1.5)zoomTo(_:animated:)
public func zoomTo(_ zoom: CGFloat, animated: Bool = true)Sets the zoom level, preserving the current center point. Zoom is clamped to [Viewport.minZoom, Viewport.maxZoom].
instance.zoomTo(1.5)
instance.zoomTo(2.0, animated: false)zoomIn / zoomOut
public func zoomIn(animated: Bool = true)
public func zoomOut(animated: Bool = true)Zoom in/out by a factor of 1.2 relative to the current zoom level.
instance.zoomIn()
instance.zoomOut()
instance.zoomIn(animated: false)reset(animated:)
public func reset(animated: Bool = true)Resets the viewport to identity (x: 0, y: 0, zoom: 1.0).
instance.reset()
instance.reset(animated: false)Graph Editing
deleteElements(nodeIds:edgeIds:)
public func deleteElements(nodeIds: [String] = [], edgeIds: [String] = [])Deletes the specified nodes and edges through the attached canvas. Requested elements are removed only when their deletable flag is true; deletable nodes are removed together with their deletable descendants (the parentId chain) and all connected edges.
Button("Delete Selected Node") {
instance.deleteElements(nodeIds: ["node-1"])
}Coordinate Conversion
screenToFlowPosition(_😃
public func screenToFlowPosition(_ screenPoint: CGPoint) -> CGPointConverts a screen-space point to flow (canvas) coordinates, accounting for current pan and zoom.
let dropPoint = instance.screenToFlowPosition(CGPoint(x: 300, y: 200))
// Create a node at the drop position
let newNode = Node(id: UUID().uuidString, position: XYPosition(x: dropPoint.x, y: dropPoint.y), data: ...)flowToScreenPosition(_😃
public func flowToScreenPosition(_ flowPoint: CGPoint) -> CGPointConverts a flow (canvas) coordinate to a screen-space position.
let screenPos = instance.flowToScreenPosition(CGPoint(x: node.position.x, y: node.position.y))
// Position a popover at screenPosBounds Calculation
getNodesBounds(nodes:nodeSizes:)
public func getNodesBounds<T: Equatable & Sendable>(
nodes: [Node<T>],
nodeSizes: [String: CGSize]
) -> CGRectReturns the bounding rectangle of the visible nodes, respecting their measured sizes. Parent-relative child positions are resolved; unmeasured nodes use their explicit width/height or 150×50.
let bounds = instance.getNodesBounds(nodes: selectedNodes, nodeSizes: instance.nodeSizes)
print("Width: \(bounds.width), Height: \(bounds.height)")getViewportForBounds(bounds:minZoom:maxZoom:padding:)
public func getViewportForBounds(
bounds: CGRect,
minZoom: CGFloat = Viewport.minZoom,
maxZoom: CGFloat = Viewport.maxZoom,
padding: CGFloat = 80
) -> ViewportComputes the optimal viewport to display the given bounding rectangle.
let bounds = instance.getNodesBounds(nodes: nodes, nodeSizes: instance.nodeSizes)
let optimalViewport = instance.getViewportForBounds(
bounds: bounds,
minZoom: 0.5,
maxZoom: 2.0,
padding: 100
)
instance.setViewport(optimalViewport)Viewport Change Callback
instance.onViewportChange = { viewport in
print("Viewport changed: \(viewport)")
}Environment Access
Access the instance from child views via the environment:
struct CustomNodeView: View {
let node: Node<MyData>
@Environment(\.swiftFlowInstance) var instance
var body: some View {
VStack {
Text(node.data.label)
Button("Focus") {
instance?.setCenter(x: node.position.x, y: node.position.y, zoom: 1.5)
}
}
}
}