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: Aprivate(set) varexposed through@Published-compatible propertiesplaybackRate: Updated viasetPlaybackRate(_:)and bound to the view aseditor.playbackRate- Playhead position: Driven by a time observer installed in
VideoEnginethat 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
VideoEngineoperates on the main actor but offloads heavy work to background tasks, isolating UI-bound operations from processing overhead.visualRefreshActionat line 623 selectively suppresses UI updates during high-speed playback, returning.nonewhen audio metering is disabled.playheadObserverIntervaluses anonisolatedstatic 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. EditorViewModelexposes only essential@Publishedproperties (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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →