import QuickLook import SwiftUI import UIKit // 🤖 A photo opened from a chat bubble: the full-res bytes staged to a temp file // for Quick Look, plus the thumbnail + its on-screen frame so the preview can // ZOOM out of the bubble it came from. struct QuickLookItem: Identifiable { let id: String let url: URL // staged temp file (full-res decrypted bytes) let thumbnail: UIImage // zoom-transition anchor image let sourceFrame: CGRect // the tapped thumbnail's window frame } // 🤖 Presents QLPreviewController IMPERATIVELY (not via .sheet) so Quick Look // owns the transition: given a transition view, it zooms the photo out of its // bubble on open and back into it on dismiss — the Photos/Files feel — and // carries Apple's own chrome plus the system Share/Save action button, so there // are no hand-rolled controls to style. The representable itself is an invisible // host; the preview is presented from the window's top-most controller and torn // down (with the caller's onClose) when Quick Look dismisses. struct ImageQuickLookPresenter: UIViewControllerRepresentable { @Binding var item: QuickLookItem? let onClose: () -> Void func makeCoordinator() -> Coordinator { Coordinator(onClose: onClose) } func makeUIViewController(context: Context) -> UIViewController { UIViewController() } func updateUIViewController(_ host: UIViewController, context: Context) { context.coordinator.sync(item: item, host: host) } final class Coordinator: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate { private let onClose: () -> Void private var current: QuickLookItem? private weak var preview: QLPreviewController? private var transitionView: UIImageView? private var presenting = false init(onClose: @escaping () -> Void) { self.onClose = onClose } // Present when an item arrives; dismiss if it's cleared while still up. // Presentation is deferred a runloop turn so it never runs mid-update. func sync(item: QuickLookItem?, host: UIViewController) { // Boot Quick Look's out-of-process renderer once, invisibly, so the // first real tap doesn't flash its cold-start chrome. QuickLookWarmup.warmIfNeeded(scene: host.view.window?.windowScene) if let item, !presenting { current = item presenting = true DispatchQueue.main.async { [weak self, weak host] in guard let self, let host else { return } self.present(from: host) } } else if item == nil, presenting { preview?.dismiss(animated: true) } } private func present(from host: UIViewController) { guard let window = host.view.window else { presenting = false; return } var top = window.rootViewController while let next = top?.presentedViewController { top = next } guard let top else { presenting = false; return } let ql = QLPreviewController() ql.dataSource = self ql.delegate = self preview = ql top.present(ql, animated: true) } // MARK: - Data source func numberOfPreviewItems(in controller: QLPreviewController) -> Int { current == nil ? 0 : 1 } func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { PreviewFile(url: current?.url) } // MARK: - Delegate (the zoom anchor) // A one-off UIImageView pinned at the thumbnail's window frame, showing // the same downsampled image. Quick Look zooms its preview out of this // view on open and back into it on dismiss. Reused across the open/close // calls and removed once the preview is gone. func previewController(_ controller: QLPreviewController, transitionViewFor item: QLPreviewItem) -> UIView? { if let transitionView { return transitionView } guard let current, let window = controller.view.window ?? preview?.presentingViewController?.view.window else { return nil } let iv = UIImageView(image: current.thumbnail) iv.frame = current.sourceFrame iv.contentMode = .scaleAspectFill iv.clipsToBounds = true iv.layer.cornerRadius = 18 iv.layer.cornerCurve = .continuous window.addSubview(iv) transitionView = iv return iv } // The anchor lives on the window, ABOVE the chat header — so when Quick // Look reveals it at the tail of the zoom-back it would briefly draw // over the header/avatar (the flash). Fade it out across the dismiss so // there's nothing to flash: the real, header-clipped thumbnail is // already underneath. Quick Look cross-fades its shrinking snapshot into // this view too, so a fading target also fades the zoom itself. func previewControllerWillDismiss(_ controller: QLPreviewController) { guard let transitionView else { return } UIView.animate(withDuration: 0.3) { transitionView.alpha = 0 } } func previewControllerDidDismiss(_ controller: QLPreviewController) { transitionView?.removeFromSuperview() transitionView = nil preview = nil presenting = false current = nil onClose() } } // Quick Look reads the file by URL. A nil URL can't reach here: the data // source reports 0 items until an item exists. private final class PreviewFile: NSObject, QLPreviewItem { let previewItemURL: URL? init(url: URL?) { previewItemURL = url } } } // 🤖 Boots Quick Look's out-of-process render service ONCE per launch so the // first real preview doesn't flash its cold-start chrome. Presents a preview of // a throwaway file on a 1pt, fully-transparent window behind everything, then // tears it (and the file) straight back down — the user never sees it, but the // service is warm by the time they tap a photo. @MainActor enum QuickLookWarmup { private static var done = false private static var window: UIWindow? private static var source: Source? static func warmIfNeeded(scene: UIWindowScene?) { guard !done, let scene else { return } done = true guard let url = try? stageDummy() else { return } let src = Source(url: url) source = src let w = UIWindow(windowScene: scene) w.frame = CGRect(x: 0, y: 0, width: 1, height: 1) w.windowLevel = UIWindow.Level(rawValue: -1000) // behind the real UI w.alpha = 0.01 // effectively invisible w.isHidden = false let root = UIViewController() w.rootViewController = root window = w let ql = QLPreviewController() ql.dataSource = src root.present(ql, animated: false) { root.dismiss(animated: false) { window?.isHidden = true window = nil source = nil try? FileManager.default.removeItem(at: url.deletingLastPathComponent()) } } } private static func stageDummy() throws -> URL { let dir = FileManager.default.temporaryDirectory .appendingPathComponent("ql-warm-\(UUID().uuidString)", isDirectory: true) try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) let url = dir.appendingPathComponent("warm.txt") try Data(" ".utf8).write(to: url) return url } final class Source: NSObject, QLPreviewControllerDataSource { private let item: Item init(url: URL) { item = Item(url: url) } func numberOfPreviewItems(in controller: QLPreviewController) -> Int { 1 } func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem { item } } private final class Item: NSObject, QLPreviewItem { let previewItemURL: URL? init(url: URL) { previewItemURL = url } } }