eleven / ocho Public

Clone
git clone https://amadan.net/eleven/ocho

Plain git — no account needed to clone.

Download

Download this file

1import QuickLook
2import SwiftUI
3import UIKit
4
5// πŸ€– A photo opened from a chat bubble: the full-res bytes staged to a temp file
6// for Quick Look, plus the thumbnail + its on-screen frame so the preview can
7// ZOOM out of the bubble it came from.
8struct QuickLookItem: Identifiable {
9 let id: String
10 let url: URL // staged temp file (full-res decrypted bytes)
11 let thumbnail: UIImage // zoom-transition anchor image
12 let sourceFrame: CGRect // the tapped thumbnail's window frame
13}
14
15// πŸ€– Presents QLPreviewController IMPERATIVELY (not via .sheet) so Quick Look
16// owns the transition: given a transition view, it zooms the photo out of its
17// bubble on open and back into it on dismiss β€” the Photos/Files feel β€” and
18// carries Apple's own chrome plus the system Share/Save action button, so there
19// are no hand-rolled controls to style. The representable itself is an invisible
20// host; the preview is presented from the window's top-most controller and torn
21// down (with the caller's onClose) when Quick Look dismisses.
22struct ImageQuickLookPresenter: UIViewControllerRepresentable {
23 @Binding var item: QuickLookItem?
24 let onClose: () -> Void
25
26 func makeCoordinator() -> Coordinator { Coordinator(onClose: onClose) }
27 func makeUIViewController(context: Context) -> UIViewController { UIViewController() }
28
29 func updateUIViewController(_ host: UIViewController, context: Context) {
30 context.coordinator.sync(item: item, host: host)
31 }
32
33 final class Coordinator: NSObject, QLPreviewControllerDataSource, QLPreviewControllerDelegate {
34 private let onClose: () -> Void
35 private var current: QuickLookItem?
36 private weak var preview: QLPreviewController?
37 private var transitionView: UIImageView?
38 private var presenting = false
39
40 init(onClose: @escaping () -> Void) { self.onClose = onClose }
41
42 // Present when an item arrives; dismiss if it's cleared while still up.
43 // Presentation is deferred a runloop turn so it never runs mid-update.
44 func sync(item: QuickLookItem?, host: UIViewController) {
45 // Boot Quick Look's out-of-process renderer once, invisibly, so the
46 // first real tap doesn't flash its cold-start chrome.
47 QuickLookWarmup.warmIfNeeded(scene: host.view.window?.windowScene)
48 if let item, !presenting {
49 current = item
50 presenting = true
51 DispatchQueue.main.async { [weak self, weak host] in
52 guard let self, let host else { return }
53 self.present(from: host)
54 }
55 } else if item == nil, presenting {
56 preview?.dismiss(animated: true)
57 }
58 }
59
60 private func present(from host: UIViewController) {
61 guard let window = host.view.window else { presenting = false; return }
62 var top = window.rootViewController
63 while let next = top?.presentedViewController { top = next }
64 guard let top else { presenting = false; return }
65 let ql = QLPreviewController()
66 ql.dataSource = self
67 ql.delegate = self
68 preview = ql
69 top.present(ql, animated: true)
70 }
71
72 // MARK: - Data source
73 func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
74 current == nil ? 0 : 1
75 }
76
77 func previewController(_ controller: QLPreviewController,
78 previewItemAt index: Int) -> QLPreviewItem {
79 PreviewFile(url: current?.url)
80 }
81
82 // MARK: - Delegate (the zoom anchor)
83 // A one-off UIImageView pinned at the thumbnail's window frame, showing
84 // the same downsampled image. Quick Look zooms its preview out of this
85 // view on open and back into it on dismiss. Reused across the open/close
86 // calls and removed once the preview is gone.
87 func previewController(_ controller: QLPreviewController,
88 transitionViewFor item: QLPreviewItem) -> UIView? {
89 if let transitionView { return transitionView }
90 guard let current,
91 let window = controller.view.window
92 ?? preview?.presentingViewController?.view.window else { return nil }
93 let iv = UIImageView(image: current.thumbnail)
94 iv.frame = current.sourceFrame
95 iv.contentMode = .scaleAspectFill
96 iv.clipsToBounds = true
97 iv.layer.cornerRadius = 18
98 iv.layer.cornerCurve = .continuous
99 window.addSubview(iv)
100 transitionView = iv
101 return iv
102 }
103
104 // The anchor lives on the window, ABOVE the chat header β€” so when Quick
105 // Look reveals it at the tail of the zoom-back it would briefly draw
106 // over the header/avatar (the flash). Fade it out across the dismiss so
107 // there's nothing to flash: the real, header-clipped thumbnail is
108 // already underneath. Quick Look cross-fades its shrinking snapshot into
109 // this view too, so a fading target also fades the zoom itself.
110 func previewControllerWillDismiss(_ controller: QLPreviewController) {
111 guard let transitionView else { return }
112 UIView.animate(withDuration: 0.3) { transitionView.alpha = 0 }
113 }
114
115 func previewControllerDidDismiss(_ controller: QLPreviewController) {
116 transitionView?.removeFromSuperview()
117 transitionView = nil
118 preview = nil
119 presenting = false
120 current = nil
121 onClose()
122 }
123 }
124
125 // Quick Look reads the file by URL. A nil URL can't reach here: the data
126 // source reports 0 items until an item exists.
127 private final class PreviewFile: NSObject, QLPreviewItem {
128 let previewItemURL: URL?
129 init(url: URL?) { previewItemURL = url }
130 }
131}
132
133// πŸ€– Boots Quick Look's out-of-process render service ONCE per launch so the
134// first real preview doesn't flash its cold-start chrome. Presents a preview of
135// a throwaway file on a 1pt, fully-transparent window behind everything, then
136// tears it (and the file) straight back down β€” the user never sees it, but the
137// service is warm by the time they tap a photo.
138@MainActor
139enum QuickLookWarmup {
140 private static var done = false
141 private static var window: UIWindow?
142 private static var source: Source?
143
144 static func warmIfNeeded(scene: UIWindowScene?) {
145 guard !done, let scene else { return }
146 done = true
147 guard let url = try? stageDummy() else { return }
148 let src = Source(url: url)
149 source = src
150
151 let w = UIWindow(windowScene: scene)
152 w.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
153 w.windowLevel = UIWindow.Level(rawValue: -1000) // behind the real UI
154 w.alpha = 0.01 // effectively invisible
155 w.isHidden = false
156 let root = UIViewController()
157 w.rootViewController = root
158 window = w
159
160 let ql = QLPreviewController()
161 ql.dataSource = src
162 root.present(ql, animated: false) {
163 root.dismiss(animated: false) {
164 window?.isHidden = true
165 window = nil
166 source = nil
167 try? FileManager.default.removeItem(at: url.deletingLastPathComponent())
168 }
169 }
170 }
171
172 private static func stageDummy() throws -> URL {
173 let dir = FileManager.default.temporaryDirectory
174 .appendingPathComponent("ql-warm-\(UUID().uuidString)", isDirectory: true)
175 try FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
176 let url = dir.appendingPathComponent("warm.txt")
177 try Data(" ".utf8).write(to: url)
178 return url
179 }
180
181 final class Source: NSObject, QLPreviewControllerDataSource {
182 private let item: Item
183 init(url: URL) { item = Item(url: url) }
184 func numberOfPreviewItems(in controller: QLPreviewController) -> Int { 1 }
185 func previewController(_ controller: QLPreviewController,
186 previewItemAt index: Int) -> QLPreviewItem { item }
187 }
188
189 private final class Item: NSObject, QLPreviewItem {
190 let previewItemURL: URL?
191 init(url: URL) { previewItemURL = url }
192 }
193}
194