Optimizing SwiftUI View Updates for High-Frequency Playback State in Palmier Pro

Palmier Pro prevents UI stalls during rapid playback by isolating high-frequency state changes in VideoEngine, using selective refresh logic via visualRefreshAction, and throttling interactions while keeping SwiftUI bindings minimal through EditorViewModel.

High-frequency playback updates can overwhelm SwiftUI’s diffing engine, causing dropped frames and unresponsive interfaces. In the palmier-io/palmier-pro codebase, the playback architecture deliberately minimizes view invalidations by isolating mutable state and computing refresh intervals off the main actor. This approach ensures that preview playback at 4× speed remains smooth without triggering unnecessary view recompositions.

Centralizing Playback Control in VideoEngine

The VideoEngine class in Sources/PalmierPro/Preview/VideoEngine.swift serves as the central playback controller and is marked with @MainActor because it interacts with UI-bound objects like AVPlayer and the preview view. However, the engine immediately offloads heavy work—such as track loading and Lottie rendering—to background tasks, ensuring the main thread remains responsive.

Rather than exposing every internal detail to SwiftUI, the engine maintains a minimal observable surface. The EditorViewModel in Sources/PalmierPro/Editor/ViewModel/EditorViewModel.swift owns the primary playback state:

  • isPlaying: A private(set) var exposed through @Published-compatible properties
  • playbackRate: Updated via setPlaybackRate(_:) and bound to the view as editor.playbackRate
  • Playhead position: Driven by a time observer installed in VideoEngine that posts updates only when the UI actually requires them

Selective UI Refresh with visualRefreshAction

At line 623 of VideoEngine.swift, the visualRefreshAction(isPlaying:playbackRate:) method decides whether the UI actually needs a refresh. When the playback rate is high—such as .quadruple—the method checks allowsAudioMetering and returns .none, preventing the view from recomputing audio-meter UI on every tick. This selective logic stops a flood of redraws during fast playback while maintaining meter updates during normal-speed preview.

The method integrates with ScrubAudioEngine to stop playback metering when high-speed flags are detected, further reducing processing overhead during rapid state changes.

Calculating Observer Intervals Off the Main Actor

To reduce main actor contention, VideoEngine declares the playheadObserverInterval(for:) function as nonisolated at line 692. Because this helper performs no UI work, Swift can execute it without hopping back onto the main actor.

The interval calculation automatically scales with the playback rate:

nonisolated static func playheadObserverInterval(for playbackRate: PreviewPlaybackRate) -> CMTime {
    let updatesPerSecond: Double = 30   // Desired UI refresh cap
    return CMTime(seconds: Double(playbackRate.rawValue) / updatesPerSecond,
                 preferredTimescale: 600)
}

This caps UI updates to 30 per second regardless of playback speed, ensuring that VideoEngine never overwhelms SwiftUI’s state processing.

Throttling Interactive Scrubbing

Interactive seeks are throttled through the interactiveThrottleTask and lastInteractiveDispatchTime properties. When users drag the playhead rapidly in PreviewContainerView.swift, the engine batches these scrub events instead of processing every single position change. This throttling mechanism limits the number of seek events delivered to the UI, preventing view churn during aggressive timeline interactions.

Isolating Mutable State in EditorViewModel

All mutable state that could trigger view updates lives inside EditorViewModel, which exposes only a few @Published-compatible properties to SwiftUI. Changes to unrelated data—such as trackMappings or clipTransforms—do not invalidate the view hierarchy because they remain isolated from the observable publisher chain.

UI components in Sources/PalmierPro/Preview/PreviewContainerView.swift bind to these specific properties rather than the engine’s raw output, creating a clean separation between high-frequency backend updates and SwiftUI’s reconciliation process.

Implementation Code Examples

The following patterns demonstrate how the UI initiates rate changes and how the engine processes them efficiently:

1. SwiftUI menu action triggering a rate change:

Button(action: { editor.setPlaybackRate(.quadruple) }) {
    Text("4×")
}

2. VideoEngine handling the update without blocking the main thread:

func setPlaybackRate(_ rate: PreviewPlaybackRate) {
    player.defaultRate = rate.rawValue
    installTimeObserver(for: rate)          // Re‑install with proper interval
    if !rate.allowsAudioMetering {
        scrubAudioEngine.stopPlaybackMetering()
    }
    if editor?.isPlaying == true, player.timeControlStatus != .paused {
        player.rate = rate.rawValue
    }
}

3. Interval calculation avoiding main actor hops:

nonisolated static func playheadObserverInterval(for playbackRate: PreviewPlaybackRate) -> CMTime {
    let updatesPerSecond: Double = 30
    return CMTime(seconds: Double(playbackRate.rawValue) / updatesPerSecond,
                 preferredTimescale: 600)
}

The MediaVisualCache.swift file complements this system by capping concurrent waveform extraction, ensuring that background media processing never interferes with the playback thread’s stability.

Summary

  • VideoEngine operates on the main actor but offloads heavy work to background tasks, isolating UI-bound operations from processing overhead.
  • visualRefreshAction at line 623 selectively suppresses UI updates during high-speed playback, returning .none when audio metering is disabled.
  • playheadObserverInterval uses a nonisolated static method to calculate time intervals without main actor contention, scaling automatically with playback rate.
  • Throttled scrubbing batches rapid seek events via interactiveThrottleTask, preventing UI churn during timeline drags.
  • EditorViewModel exposes only essential @Published properties (isPlaying, playbackRate), ensuring that structural data changes do not trigger unnecessary view recompositions.

Frequently Asked Questions

How does Palmier Pro prevent UI lag during fast playback?

The engine calls visualRefreshAction to determine if the UI actually needs updating. At rates like .quadruple, it returns .none and disables audio metering, preventing SwiftUI from recomputing expensive meter views on every frame tick. This selective refresh logic is verified in Tests/PalmierProTests/Editor/PreviewPlaybackRateTests.swift.

Why is the playheadObserverInterval method marked nonisolated?

Marking playheadObserverInterval as nonisolated (line 692) allows Swift to execute the interval calculation without switching to the main actor. Since the function performs pure computation with no UI side effects, this eliminates unnecessary context switches and reduces contention during high-frequency playback updates.

What role does EditorViewModel play in optimizing SwiftUI updates?

EditorViewModel acts as a gatekeeper by exposing only isPlaying and playbackRate as @Published properties. All other mutable state—such as clip transforms and track mappings—remains private, ensuring that background calculations never invalidate the SwiftUI view hierarchy unless explicitly intended.

How does throttling improve scrubbing performance?

The interactiveThrottleTask and lastInteractiveDispatchTime properties in VideoEngine batch rapid seek events when users drag the playhead. Instead of processing every position change, the engine delivers limited, timed updates to the UI, preventing the view system from choking on high-frequency interaction events.

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 →