SwiftUI Custom Timeline View Implementation: The Palmier Pro Architecture

Palmier Pro achieves editor-grade timeline performance by bridging an AppKit NSView into SwiftUI via NSViewRepresentable, combining GPU-accelerated drawing with reactive state synchronization through a dedicated Coordinator pattern.

Building a high-performance timeline in SwiftUI requires breaking out of the standard view hierarchy for demanding graphics operations. Palmier Pro implements a hybrid SwiftUI custom timeline view architecture that wraps a custom AppKit drawing surface inside a SwiftUI container. This approach delivers the smooth scrolling, precise hit-testing, and frame-accurate updates required for professional video editing while remaining fully composable within native SwiftUI view trees.

Architecture Overview

The Palmier Pro timeline separates concerns across five distinct layers, each optimized for specific responsibilities. This design isolates rendering performance from state management and input handling.

  • SwiftUI Wrapper (TimelineContainerView): Exposes the timeline as an NSViewRepresentable for placement in SwiftUI hierarchies.
  • AppKit Drawing View (TimelineView): Handles fast redraws of tracks, clips, and playheads using direct layer rendering in Sources/PalmierPro/Timeline/TimelineView.swift.
  • Geometry Model (TimelineGeometry): Pure-Swift struct in Sources/PalmierPro/Timeline/TimelineGeometry.swift managing frame-to-coordinate conversions.
  • Input Controller (TimelineInputController): Delegates mouse and drag events from the view to the editor model (referenced in architecture but implemented separately).
  • Editor Model (EditorViewModel): Holds timeline data and drives rendering state through published changes in Sources/PalmierPro/Editor/EditorViewModel.swift.

Bridging SwiftUI with AppKit

The TimelineContainerView implements NSViewRepresentable to embed AppKit components within SwiftUI. It constructs three sub-views: a header for track names (TimelineHeaderView), an NSScrollView hosting the drawing canvas, and a border line.

The implementation stores references to these sub-views in a Coordinator class, enabling the SwiftUI side to trigger AppKit redraws and synchronize scroll position with playback state.

// TimelineContainerView.swift – makeNSView implementation
let container = NSView()
let headerView = TimelineHeaderView(editor: editor)
let scrollView = NSScrollView()
let timelineView = TimelineView(editor: editor)
scrollView.documentView = timelineView

When the editor model updates, the updateNSView method invokes needsDisplay = true on the timeline view and adjusts content sizes dynamically.

The Core AppKit Drawing View

TimelineView inherits from NSView and performs all rendering directly into its layer, bypassing SwiftUI's rendering overhead for complex timelines. The view owns a TimelineCanvasView for final drawing commands and hosts SwiftUI overlays—such as PlayheadOverlay, SnapIndicatorOverlay, and ClipGeneratingOverlay—as NSHostingView instances.

This hybrid approach allows GPU-accelerated background drawing while supporting rich SwiftUI overlay content. Key rendering steps include:

  1. Canvas Layout: layoutCanvas matches the visible rectangle to the scroll view bounds.
  2. Content Sizing: updateContentSize recalculates dimensions when frame counts, zoom scales, or track lists change.
  3. Direct Drawing: The canvas draws using pre-computed clipDisplayRects for optimal performance.
// In TimelineView.swift – GPU-accelerated background setup
layer?.backgroundColor = AppTheme.Background.surface.cgColor
wantsLayer = true

Geometry and Hit-Testing Logic

TimelineGeometry provides pure-Swift, side-effect-free calculations for coordinate conversion and spatial detection. This struct knows the pixel-per-frame scale, header width, and per-track heights, enabling thread-safe use in both rendering and input handling.

The struct exposes conversion methods critical for SwiftUI custom timeline view implementations:

  • xForFrame(_:) and frameAt(x:) translate between frame numbers and X positions.
  • trackAt(y:) maps Y coordinates to track indices.
  • dropTargetAt(y:) determines whether a drag operation creates a new track or drops onto existing content.
  • insertionLineY(for:) and ghostY(for:) calculate visual positions for drag feedback.

Editor Model Integration

The EditorViewModel owns the timeline data and publishes render-state changes. The Coordinator observes these changes and triggers specific updates:

  • timelineView?.updateContentSize() when the layout requires expansion.
  • timelineView?.needsDisplay = true when visual elements refresh.
  • Scroll-to-playhead logic during active playback via updateNSView.

This tight coupling ensures the UI remains synchronized with the model without redundant recomputation or frame drops.

Implementation Examples

Embedding the timeline in a SwiftUI view requires only the container wrapper and environment injection:

struct EditorScreen: View {
    @Environment(EditorViewModel.self) private var editor
    
    var body: some View {
        TimelineContainerView()
            .frame(maxWidth: .infinity, maxHeight: .infinity)
            .environment(editor)
    }
}

To customize the timeline background, modify the layer property in TimelineView.swift:

layer?.backgroundColor = AppTheme.Background.surface.cgColor

For adding SwiftUI overlays like selection marquees, wrap them in NSHostingView:

// Inside TimelineView.swift
private var selectionOverlay: NSHostingView<SelectionOverlay>!

func setupSelectionOverlay() {
    selectionOverlay = NSHostingView(rootView: SelectionOverlay())
    addSubview(selectionOverlay)
}

Summary

  • Palmier Pro implements a SwiftUI custom timeline view by wrapping an NSView in NSViewRepresentable via TimelineContainerView.
  • The AppKit drawing view (TimelineView) handles GPU-accelerated rendering using direct layer access, while NSHostingView manages SwiftUI overlays.
  • Pure-Swift geometry (TimelineGeometry) provides thread-safe coordinate conversion and hit-testing without side effects.
  • The Coordinator pattern synchronizes scroll position and redraws between the SwiftUI layer and the AppKit implementation.
  • Source files reside in Sources/PalmierPro/Timeline/ and Sources/PalmierPro/Editor/, with styling constants defined in Sources/PalmierPro/UI/AppTheme.swift.

Frequently Asked Questions

Why use AppKit NSView instead of pure SwiftUI for the timeline?

SwiftUI's rendering engine introduces overhead for complex, high-frequency updates common in video editing timelines. By using an NSView with wantsLayer = true, Palmier Pro achieves GPU-accelerated drawing and precise control over redraw regions, enabling smooth scrolling with hundreds of clips while still hosting SwiftUI overlays via NSHostingView.

How does the timeline handle drag-and-drop operations?

Drag-and-drop logic flows through TimelineGeometry methods like dropTargetAt(y:) and trackAt(y:), which calculate drop targets and track indices from mouse coordinates. These pure functions execute in the input controller to determine whether to create new tracks or place clips on existing ones, then update the EditorViewModel which triggers UI refreshes through the Coordinator.

Can I customize the timeline appearance without modifying the source?

You can customize colors by modifying the AppTheme constants in Sources/PalmierPro/UI/AppTheme.swift or by exposing the layer?.backgroundColor property through a SwiftUI binding on TimelineContainerView. For structural changes to how clips render, you must modify the drawing logic in TimelineCanvasView within TimelineView.swift.

What triggers a timeline redraw when the playhead moves?

The EditorViewModel publishes playhead position changes, which the Coordinator observes in TimelineContainerView. The Coordinator then calls timelineView?.needsDisplay = true to invalidate the AppKit view, and optionally adjusts scrollView bounds to keep the playhead visible during playback, ensuring frame-perfect synchronization without full SwiftUI hierarchy updates.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →