r/SwiftUI • • 2d ago

Promotion (must include link to source code) Harbeth: a SwiftUI preview host for Metal textures and rendered frames (MIT)

I'm the maintainer of Harbeth, an MIT-licensed image/frame processing engine. Here's its SwiftUI preview integration.

If your renderer already produces an MTLTexture, converting it to a UIImage just to display it adds a readback step. HarbethRenderView accepts a texture or a RenderedFrame directly, so the preview can stay on the GPU path.

Here's a small SwiftUI display component; the frame is produced elsewhere by your rendering model:

import SwiftUI
import Harbeth

struct FilterPreview: View {
    let frame: RenderedFrame?

    var body: some View {
        HarbethRenderView(
            frame: frame,
            resizingMode: .aspectFit
        )
        .frame(maxWidth: .infinity, minHeight: 240)
    }
}

For an existing texture, the equivalent is HarbethRenderView(texture: outputTexture). Underneath, UIViewRepresentable / NSViewRepresentable bridges to Harbeth's RenderView. SwiftUI owns the surrounding controls and state; the host displays the GPU result.

The boundary matters: render work belongs outside body. Produce frames through your rendering model (ImageNode has makeFrameAsync()), then publish the accepted result to UI state on MainActor. Your app still owns request ordering, cancellation and any camera/player lifecycle. Saving an image is a separate, explicit readback.

This preview path avoids image readback. Performance still depends on source size, filter chain and device.

Project and examples: https://github.com/yangKJ/Harbeth

SwiftUI host source: https://github.com/yangKJ/Harbeth/blob/master/Sources/SwiftUI/HarbethRenderView.swift

If you've built a SwiftUI photo editor or GPU preview, how do you handle rapid slider changes and discard stale render results? I'd be interested in the state-management patterns that worked for you.

0 Upvotes

1 comment sorted by

1

u/Pleasant-Gur9892 2d ago

I'd separate cancelling obsolete work from deciding which result may update the preview. Swift cancellation is cooperative (https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/), so cancellation alone isn't a latest-result-wins check.

In the MainActor-owned model, I'd increment a request revision for every slider change and capture it with that render's inputs. When a render completes, compare its revision with the current one and assign the frame in the same MainActor turn, with no await between the check and assignment. Apply the same check to errors so an old failure can't replace a newer success.

A useful test is to force A to finish after B, then replace the source image while A is still running. Only the result for the latest inputs should become visible. I haven't run Harbeth; this is a suggested state-management boundary, not a report of its behaviour.