How to Schedule Background Jobs in Palmier Pro: DispatchQueue and Swift Concurrency Patterns
Palmier Pro schedules background jobs using DispatchQueue.global() with quality-of-service levels or Swift's Task API, ensuring heavy operations execute off the main thread while UI updates return to the main queue via DispatchQueue.main.async or MainActor.run.
Palmier Pro is a Swift-based video editing framework that relies on intensive background processing for video generation, media caching, and asset normalization. Understanding how to schedule background jobs in Palmier Pro is essential for maintaining a responsive UI while handling computationally expensive tasks. The codebase demonstrates consistent patterns using Grand Central Dispatch (GCD) and modern Swift concurrency that you can replicate for any custom background operation.
Background Job Patterns in the Palmier Pro Codebase
The Palmier Pro repository implements background job scheduling through two primary mechanisms: traditional GCD queues for compatibility and Swift's structured concurrency for modern async flows.
The DispatchQueue Approach (GCD)
Most background work in Palmier Pro uses DispatchQueue.global() with explicit quality-of-service (QoS) levels. In Sources/PalmierPro/Generation/GenerationService.swift, the video generation service dispatches encoding work to a background queue:
DispatchQueue.global(qos: .userInitiated).async {
// Heavy video encoding work
let result = self.encodeVideoFrames()
DispatchQueue.main.async {
// Return to main thread for UI notification
self.delegate?.generationCompleted(result)
}
}
Similarly, Sources/PalmierPro/Timeline/MediaVisualCache.swift uses the .utility QoS for thumbnail decoding operations that don't require immediate user-facing results:
DispatchQueue.global(qos: .utility).async {
let thumbnail = self.decodeImageData(data)
DispatchQueue.main.async {
self.cache[assetId] = thumbnail
self.updateUI()
}
}
The Swift Concurrency Approach
For newer code paths, Palmier Pro utilizes Swift's Task API with background priorities. This pattern appears in async service methods where you need structured cancellation and async/await syntax:
Task(priority: .background) {
let normalizedData = await self.processVideoFrames()
await MainActor.run {
self.previewLayer.update(with: normalizedData)
}
}
Implementation Examples from Source Files
Video Generation with GenerationService.swift
The GenerationService.swift file demonstrates the standard pattern for long-running background jobs. It receives video generation requests, processes them on a global queue, and communicates completion back to the main thread:
Key implementation details:
- Uses
DispatchQueue.global(qos: .userInitiated)for video encoding - Wraps UI callbacks in
DispatchQueue.main.async - Handles errors on the background queue before main thread notification
func startGeneration(project: Project) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
do {
let outputURL = try self.encoder.encode(project: project)
DispatchQueue.main.async {
self.delegate?.generationSucceeded(url: outputURL)
}
} catch {
DispatchQueue.main.async {
self.delegate?.generationFailed(error: error)
}
}
}
}
Alpha Channel Processing with AlphaVideoNormalizer.swift
For isolated, resource-intensive tasks, Sources/PalmierPro/Preview/AlphaVideoNormalizer.swift creates a dedicated serial DispatchQueue rather than using the global pool:
private let processingQueue = DispatchQueue(
label: "io.palmier.alpha-normalize",
qos: .background
)
func normalize(video: AVAsset) {
processingQueue.async { [weak self] in
guard let self = self else { return }
// Per-frame pixel manipulation
let processedFrames = self.extractAndNormalizeFrames(video)
DispatchQueue.main.async {
self.completionHandler?(processedFrames)
}
}
}
This approach prevents the background task from interfering with other system-wide background operations while providing explicit control over execution context.
Media Thumbnail Caching with MediaVisualCache.swift
Lazy loading operations in Sources/PalmierPro/Timeline/MediaVisualCache.swift use the .utility QoS level, which indicates work that the user has initiated but doesn't require immediate results:
func loadThumbnail(for asset: Asset, completion: @escaping (UIImage?) -> Void) {
// Check cache first on main thread
DispatchQueue.global(qos: .utility).async { [weak self] in
guard let self = self else { return }
let image = self.decodeThumbnailData(asset.data)
DispatchQueue.main.async {
self.cache[asset.id] = image
completion(image)
}
}
}
Timer-Driven Updates with TimelineInputController.swift
While Sources/PalmierPro/Timeline/TimelineInputController.swift primarily handles UI interactions, it demonstrates how to integrate background checkpoints. When auto-scrolling the playhead during drag operations, any heavy calculations get dispatched to background queues before updating the scroll position:
func handleDrag(with velocity: CGFloat) {
// Lightweight timer runs on main run loop
DispatchQueue.global(qos: .userInitiated).async {
let newPosition = self.calculateScrollPosition(velocity: velocity)
DispatchQueue.main.async {
self.scrollView.contentOffset = newPosition
}
}
}
Step-by-Step Guide to Scheduling Background Jobs
Follow these steps to implement background job scheduling consistent with Palmier Pro's architecture:
-
Select the appropriate QoS level based on task urgency:
.userInitiatedfor video generation and exports that the user is actively waiting for.utilityfor thumbnail generation and caching that improves experience but isn't immediately required.backgroundfor maintenance tasks and alpha processing that can run when system resources permit
-
Dispatch the work using either global queues or dedicated serial queues:
// For most operations DispatchQueue.global(qos: .userInitiated).async { // Expensive work here } // For isolated, long-running tasks let jobQueue = DispatchQueue(label: "io.palmier.custom-job", qos: .background) jobQueue.async { // Isolated work here } -
Return to the main thread for all UI updates:
DispatchQueue.main.async { self.updateInterface(with: results) } -
Handle cancellation by storing work item references:
private var generationWorkItem: DispatchWorkItem? func startJob() { generationWorkItem = DispatchWorkItem { [weak self] in self?.performWork() } DispatchQueue.global(qos: .userInitiated).async(execute: generationWorkItem!) } func cancelJob() { generationWorkItem?.cancel() }
Best Practices for Background Job Management
Queue Configuration and QoS Levels
Palmier Pro categorizes background jobs using specific QoS levels to ensure system resources are allocated appropriately. Use .userInitiated for video encoding tasks that block user interaction, .utility for media caching, and .background for pixel normalization that can proceed opportunistically.
Thread Safety and State Management
When accessing shared mutable state from background queues, protect data with serial dispatch queues or actor isolation. The MediaVisualCache.swift implementation caches thumbnails on the main thread after background decoding completes, ensuring thread-safe access to the dictionary:
// Background thread
let decodedImage = decodeData(data)
// Main thread only
DispatchQueue.main.async {
self.cache[key] = decodedImage // Thread-safe cache update
}
For Combine-based pipelines, use receive(on: DispatchQueue.main) to ensure UI updates occur on the correct thread:
generationService.publisher
.receive(on: DispatchQueue.main)
.sink { [weak self] result in
self?.updateUI(result)
}
.store(in: &cancellables)
Summary
- Palmier Pro uses
DispatchQueue.global()with explicit QoS levels (.userInitiated,.utility,.background) to schedule background jobs inGenerationService.swift,MediaVisualCache.swift, andAlphaVideoNormalizer.swift. - Always return to the main thread using
DispatchQueue.main.asyncorMainActor.runbefore updating UI elements or modifying shared state accessed by the main thread. - Create dedicated serial queues (as shown in
AlphaVideoNormalizer.swift) when you need isolation from other background operations or require strict FIFO execution order. - Store
DispatchWorkItemreferences if you need to cancel background jobs before completion. - Prefer Swift's
TaskAPI withMainActor.runfor new code using async/await patterns, following the modern concurrency approach found in newer Palmier Pro modules.
Frequently Asked Questions
How does Palmier Pro handle UI updates after background work completes?
Palmier Pro strictly requires all UI updates to occur on the main thread. After completing background work in DispatchQueue.global().async blocks, the code wraps interface updates in DispatchQueue.main.async closures. For Swift concurrency, it uses await MainActor.run { ... } to hop back to the main actor before modifying UI state.
What QoS level should I use for background jobs in Palmier Pro?
Choose .userInitiated for operations the user is actively waiting on (like video exports), .utility for tasks that improve the experience but aren't immediately required (like thumbnail caching in MediaVisualCache.swift), and .background for maintenance tasks that can run opportunistically (like alpha channel normalization in AlphaVideoNormalizer.swift).
Can I use Swift's async/await instead of DispatchQueue for background jobs?
Yes. Palmier Pro supports modern Swift concurrency using Task(priority: .background) to launch background work. Use await for asynchronous operations, then call await MainActor.run { ... } to return to the main thread for UI updates. This pattern provides structured cancellation and cleaner syntax compared to traditional GCD blocks.
How do I cancel a scheduled background job in Palmier Pro?
Store a reference to the DispatchWorkItem when creating the job, then call cancel() on that reference. For Swift concurrency tasks, keep a reference to the Task instance and call task.cancel(). Check isCancelled inside your background block to exit early and avoid unnecessary computation.
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 →