Skip to content

Auto Layout Example ​

Demonstrates all three layout algorithms (tree, force-directed, grid) with live switching.

swift
import SwiftUI
import SwiftFlow

// MARK: - Data Models

struct LayoutNodeData: Equatable, Sendable, Codable {
    var label: String
}

// MARK: - Content View

struct AutoLayoutExample: View {
    @State var nodes: [Node<LayoutNodeData>] = makeInitialNodes()
    @State var edges: [FlowEdge<EmptyEdgeData>] = makeInitialEdges()
    @State var activeLayout: String = "tree"

    var body: some View {
        VStack(spacing: 0) {
            // Layout selector
            HStack(spacing: 8) {
                Text("Layout:")
                    .font(.caption).foregroundColor(.secondary)

                layoutButton("Tree", "tree")
                layoutButton("Force", "force")
                layoutButton("Grid", "grid")
                layoutButton("L→R", "leftright")

                Spacer()

                Button("Reset") {
                    nodes = makeInitialNodes()
                    edges = makeInitialEdges()
                }
                .buttonStyle(.bordered)
                .controlSize(.small)
            }
            .padding(.horizontal)
            .padding(.vertical, 8)
            .background(.ultraThinMaterial)

            SwiftFlow(
                nodes: nodes,
                edges: edges,
                onNodesChange: { nodes = applyNodeChanges($0, nodes: nodes) },
                onEdgesChange: { edges = applyEdgeChanges($0, edges: edges) },
                onConnect: { edges = addEdge($0, edges: edges,
                    defaults: DefaultEdgeOptions(
                        type: .smoothstep,
                        markerEnd: .arrowClosed
                    ))
                },
                fitView: true,
                fitViewOptions: FitViewOptions(padding: 60, maxZoom: 2.0, duration: 0.4)
            ) { node in
                HStack(spacing: 0) {
                    Handle(nodeId: node.id, id: "in", type: .target, position: .left)
                    Text(node.data.label)
                        .font(.system(size: 12))
                        .padding(.horizontal, 12)
                        .padding(.vertical, 8)
                    Handle(nodeId: node.id, id: "out", type: .source, position: .right)
                }
                .background(
                    RoundedRectangle(cornerRadius: 6)
                        .fill(.white)
                        .shadow(color: .black.opacity(0.06), radius: 2, y: 1)
                )
                .overlay(
                    RoundedRectangle(cornerRadius: 6)
                        .stroke(node.selected ? .blue : .gray.opacity(0.3), lineWidth: 1)
                )
            } overlay: {
                Background(variant: .dots, gap: 30)
                Controls(position: .bottomLeft)
                MiniMap()
            }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
    }

    // MARK: - Layout Buttons

    func layoutButton(_ title: String, _ id: String) -> some View {
        Button(title) {
            activeLayout = id
            applyLayout(id)
        }
        .buttonStyle(.borderedProminent)
        .controlSize(.small)
        .tint(activeLayout == id ? .blue : .gray.opacity(0.3))
    }

    func applyLayout(_ id: String) {
        let algorithm: LayoutAlgorithm
        switch id {
        case "tree":
            algorithm = .tree(direction: .topToBottom, nodeSpacing: 60, levelSpacing: 120)
        case "force":
            algorithm = .forceDirected(iterations: 120, idealLength: 200, repulsion: 6000)
        case "grid":
            algorithm = .grid(columns: 4, nodeSpacing: 80)
        case "leftright":
            algorithm = .tree(direction: .leftToRight, nodeSpacing: 60, levelSpacing: 150)
        default:
            return
        }

        let changes = computeAutoLayout(
            nodes: nodes,
            edges: edges,
            algorithm: algorithm
        )
        nodes = applyNodeChanges(changes, nodes: nodes)
    }
}

// MARK: - Initial Graph

func makeInitialNodes() -> [Node<LayoutNodeData>] {
    [
        Node(id: "root",  position: .zero, data: LayoutNodeData(label: "Root")),
        Node(id: "a1",   position: .zero, data: LayoutNodeData(label: "Child A1")),
        Node(id: "a2",   position: .zero, data: LayoutNodeData(label: "Child A2")),
        Node(id: "a3",   position: .zero, data: LayoutNodeData(label: "Child A3")),
        Node(id: "b1",   position: .zero, data: LayoutNodeData(label: "Child B1")),
        Node(id: "b2",   position: .zero, data: LayoutNodeData(label: "Child B2")),
        Node(id: "c1",   position: .zero, data: LayoutNodeData(label: "Leaf C1")),
        Node(id: "c2",   position: .zero, data: LayoutNodeData(label: "Leaf C2")),
        Node(id: "c3",   position: .zero, data: LayoutNodeData(label: "Leaf C3")),
        Node(id: "orphan", position: .zero, data: LayoutNodeData(label: "Orphan")),
    ]
}

func makeInitialEdges() -> [FlowEdge<EmptyEdgeData>] {
    [
        FlowEdge(id: "e-r-a1", source: "root", target: "a1",
             markerEnd: .arrowClosed),
        FlowEdge(id: "e-r-a2", source: "root", target: "a2",
             markerEnd: .arrowClosed),
        FlowEdge(id: "e-r-a3", source: "root", target: "a3",
             markerEnd: .arrowClosed),
        FlowEdge(id: "e-a1-b1", source: "a1", target: "b1",
             markerEnd: .arrowClosed),
        FlowEdge(id: "e-a1-b2", source: "a1", target: "b2",
             markerEnd: .arrowClosed),
        FlowEdge(id: "e-b1-c1", source: "b1", target: "c1",
             markerEnd: .arrowClosed),
        FlowEdge(id: "e-b1-c2", source: "b1", target: "c2",
             markerEnd: .arrowClosed),
        FlowEdge(id: "e-b2-c3", source: "b2", target: "c3",
             markerEnd: .arrowClosed),
    ]
}

// MARK: - Preview

#Preview {
    AutoLayoutExample()
}

What This Example Demonstrates ​

  • Tree layout (top-to-bottom): Hierarchical arrangement using BFS level assignment
  • Tree layout (left-to-right): Same algorithm with horizontal flow direction
  • Force-directed layout: Physics simulation with repulsion/attraction forces
  • Grid layout: Simple row-major grid arrangement
  • computeAutoLayout: Returns [NodeChange] for clean integration with the change pipeline
  • Orphan nodes: The "Orphan" node has no edges — it's placed at level 0 in tree layout and gravitates to center in force layout
  • Live switching: Change layouts at runtime without rebuilding the graph
  • Fit view: Auto-fits all nodes after layout changes with smooth animation

SwiftUI-native node graph editor.