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:

  1. Select the appropriate QoS level based on task urgency:

    • .userInitiated for video generation and exports that the user is actively waiting for
    • .utility for thumbnail generation and caching that improves experience but isn't immediately required
    • .background for maintenance tasks and alpha processing that can run when system resources permit
  2. 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
    }
  3. Return to the main thread for all UI updates:

    DispatchQueue.main.async {
        self.updateInterface(with: results)
    }
  4. 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 in GenerationService.swift, MediaVisualCache.swift, and AlphaVideoNormalizer.swift.
  • Always return to the main thread using DispatchQueue.main.async or MainActor.run before 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 DispatchWorkItem references if you need to cancel background jobs before completion.
  • Prefer Swift's Task API with MainActor.run for 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:

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 →