# How to Schedule Background Jobs in Palmier Pro: DispatchQueue and Swift Concurrency Patterns

> Learn to schedule background jobs in Palmier Pro using DispatchQueue or Swift Concurrency. Optimize performance by running heavy operations off the main thread for a smoother app experience.

- Repository: [Palmier/palmier-pro](https://github.com/palmier-io/palmier-pro)
- Tags: how-to-guide
- Published: 2026-06-21

---

**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`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Generation/GenerationService.swift), the video generation service dispatches encoding work to a background queue:

```swift
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`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Timeline/MediaVisualCache.swift) uses the `.utility` QoS for thumbnail decoding operations that don't require immediate user-facing results:

```swift
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:

```swift
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`](https://github.com/palmier-io/palmier-pro/blob/main/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

```swift
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`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Preview/AlphaVideoNormalizer.swift) creates a dedicated serial `DispatchQueue` rather than using the global pool:

```swift
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`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Timeline/MediaVisualCache.swift) use the `.utility` QoS level, which indicates work that the user has initiated but doesn't require immediate results:

```swift
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`](https://github.com/palmier-io/palmier-pro/blob/main/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:

```swift
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:

   ```swift
   // 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:

   ```swift
   DispatchQueue.main.async {
       self.updateInterface(with: results)
   }
   ```

4. **Handle cancellation** by storing work item references:

   ```swift
   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`](https://github.com/palmier-io/palmier-pro/blob/main/MediaVisualCache.swift) implementation caches thumbnails on the main thread after background decoding completes, ensuring thread-safe access to the dictionary:

```swift
// 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:

```swift
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`](https://github.com/palmier-io/palmier-pro/blob/main/GenerationService.swift), [`MediaVisualCache.swift`](https://github.com/palmier-io/palmier-pro/blob/main/MediaVisualCache.swift), and [`AlphaVideoNormalizer.swift`](https://github.com/palmier-io/palmier-pro/blob/main/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`](https://github.com/palmier-io/palmier-pro/blob/main/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`](https://github.com/palmier-io/palmier-pro/blob/main/MediaVisualCache.swift)), and `.background` for maintenance tasks that can run opportunistically (like alpha channel normalization in [`AlphaVideoNormalizer.swift`](https://github.com/palmier-io/palmier-pro/blob/main/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.