Reading a long article with the iPhone's Camera Control
Get in touch for more detailsThis interaction is a long-form article screen that you navigate almost entirely through the hardware Camera Control on iPhone 16 and above. The reading surface itself is pure SwiftUI: a ScrollView + ScrollViewReader with iOS 18's ScrollPosition and onScrollGeometryChange for precise offset tracking, Text + AttributedString for the article body and search highlighting, and a UIViewRepresentable-wrapped UIVisualEffectView + CAGradientLayer for the variable blur at the bottom (SwiftUI has no gradient-masked blur). On top of that, four features (Scroll, Highlight, Search, and Theme) each map to a native AVCaptureControl, and you cycle between them with a double light-press, exactly like the Camera app switches zoom and exposure.
The trick that makes it feel like a native control is that an AVCaptureSession is genuinely running behind the reading UI the whole time. That's the camera behind. The Camera Control is a hardware feature owned by AVFoundation, so it only comes alive when three things are true simultaneously: a camera input is attached (and supportsControls is checked after that), a AVCaptureVideoPreviewLayer is connected to the session (not necessarily visible, we keep it behind an opaque cover), and an active AVCaptureEventInteraction lives in the view hierarchy. Miss any one and the delegate never fires. Layout-wise, that means a small stack: an @MainActor @Observable controller the SwiftUI view watches, an @unchecked Sendable core that owns every AVFoundation object on one serial queue, and two thin representables (preview host + event catcher) sitting invisibly beneath the article.
Because the slider's value is read-write, scrolling is content-aware and absolute: when a control activates we sync the slider back to the reader's current position, so you can scrub in both directions from anywhere (no "stuck at the bottom" problem), and the slider's range is sized dynamically to the article's length so value-0 is the top and value-max is the end. The tradeoffs are honest and worth calling out: the green camera dot stays lit while the session runs, the system HUD appears on every swipe, and the whole thing needs a physical iPhone 16 (the simulator falls back to an on-screen debug slider).
AVCaptureVideoPreviewLayerconnected, hidden
AVCaptureEventInteractionactive
camera inputattached
configs · setScrollValue
onEvent · active-change
→ Task { @MainActor }
session runs the whole time
→ privacy dot stays on
double light-press cycles
scroll = slider · sections
theme = index-picker
slider.value is read-write→ sync to current position on activate = content-aware, scrubs both directions
range = maxScroll / (pointsPerUnit × sensitivity) → 0 = top · max = article end