kylianvermeulen/arboreal
A tree-structured drag-and-drop library for iOS. UIKit handles the heavy lifting for smooth, native interactions. SwiftUI gets a thin bridge layer so you can drop it into your views with a single component.
Features
- Drag and drop to reorder items within and across sections
- Self-sizing rows based on SwiftUI content
- Multi-selection drag support
- Collapsible sections with observable expansion state
- Live drop preview indicator with customizable theming
- Floating drag view that follows your finger
- Haptic feedback on drag, drop, hover, and error events
- Per-item and per-target validation callbacks
- Pure-function tree mutations (extract, insert, move)
- Zero external dependencies
Requirements
- iOS 18+
- Swift 6.2+
- Xcode 26+
Installation
Add Arboreal via Swift Package Manager:
https://github.com/kylianvermeulen/Arboreal.gitOr add it to your Package.swift:
dependencies: [
.package(url: "https://github.com/kylianvermeulen/Arboreal.git", from: "0.3.0")
]Quick Start
1. Define your content model
import Arboreal
struct Task: TreeNodeContent {
let id: UUID
let title: String
var isContainer: Bool { false }
}2. Build a tree
@State private var tree: [TreeNode<Task>] = [
TreeNode(
content: Task(id: UUID(), title: "Section"),
children: [
TreeNode(content: Task(id: UUID(), title: "Item A")),
TreeNode(content: Task(id: UUID(), title: "Item B")),
]
)
]
@State private var expansionState = ExpansionState<UUID>()3. Use TreeDragDropView
TreeDragDropView(
tree: tree,
expansionState: expansionState
) { item, depth, isSelected, isExpanded in
Text(item.title)
.padding(.leading, CGFloat(depth) * 20)
}Configuration
Customize behavior through TreeDragDropConfiguration:
var config = TreeDragDropConfiguration<Task>()
config.indentationWidth = 24
config.canDrag = { item in !item.isContainer }
config.onReorder = { newTree in save(newTree) }
config.dropPreviewTheme = DropPreviewTheme(
fillColor: .blue.opacity(0.12),
cornerRadius: 10,
horizontalPadding: 16
)
config.floatingDragStyle = FloatingDragStyle(
backgroundColor: .systemBackground,
cornerRadius: 12,
shadowOpacity: 0.3,
liftScale: 1.05
)
TreeDragDropView(
tree: tree,
expansionState: expansionState,
configuration: config
) { item, depth, isSelected, isExpanded in
// ...
}Key Types
| Type | Description | |------|-------------| | TreeNodeContent | Protocol your content model conforms to | | TreeNode | A tree node with content and children (max depth 1) | | TreeDragDropView | The main SwiftUI view | | TreeDragDropConfiguration | Layout, behavior, and callback settings | | ExpansionState | Observable state tracking expanded sections | | DropPreviewTheme | Drop indicator appearance | | FloatingDragStyle | Floating drag view appearance (corner radius, shadow, scale) | | HapticConfiguration | Haptic feedback toggles | | FlatTreeEntry | Flattened row representation used for layout | | DropTarget | Where a drop lands (.atIndex or .intoSection) | | DragPayload | What is being dragged (.singleItem, .multipleItems, .section) |
Tree Mutations
Pure functions on [TreeNode] for programmatic tree manipulation:
// Extract nodes by ID
let (remaining, extracted) = tree.extractingNodes(ids: selectedIDs)
// Insert at a target position
let updated = tree.insertingNodes(nodes, at: .atIndex(parentID: sectionID, index: 0))
// Move nodes to a new position
let moved = tree.movingNodes(ids: selectedIDs, to: .intoSection(sectionID))
// Check if a drop is valid
let allowed = tree.canDrop(draggedIDs: selectedIDs, onto: target)
// Flatten for layout
let entries = tree.flattened(expansionState: expansionState.expandedIDs)Example
See Examples/Example/ for a working demo showing sections, tasks, expansion, and reordering.
License
MIT -- see LICENSE.
Package Metadata
Repository: kylianvermeulen/arboreal
Default branch: master
README: README.md