Add iOS peek long-press context menu for image embeds
Introduces expo-bluesky-context-menu, a compositional Root/Trigger/Menu/MenuItem wrapper around UIContextMenuInteraction. The preview ViewController is sized to the image's true aspect ratio so tall/panorama previews don't stretch mid-lift, and menu icons are rasterized from the app's SVG icon set on the native side. The preview prop is a discriminated union (image today; video and externalCard reserved) so the same module can back those embeds in follow-ups. https://claude.ai/code/session_015REmux3R9uuEMMJUHxTyQT
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import UIKit
|
||||
|
||||
/// Preview view controller shown during a peek. Renders a single image sized
|
||||
/// to the provided aspect ratio, capped to the screen bounds.
|
||||
///
|
||||
/// The aspect ratio drives `preferredContentSize` so iOS animates directly to
|
||||
/// the final size without the mid-flight stretch that happens when a mis-sized
|
||||
/// snapshot is scaled up.
|
||||
final class ImagePreviewController: UIViewController {
|
||||
private let imageURL: URL?
|
||||
private let aspectRatio: CGFloat
|
||||
|
||||
init(imageURL: URL?, aspectRatio: CGFloat) {
|
||||
self.imageURL = imageURL
|
||||
self.aspectRatio = aspectRatio.isFinite && aspectRatio > 0 ? aspectRatio : 1
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
self.preferredContentSize = Self.sizeForAspect(self.aspectRatio)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) not supported") }
|
||||
|
||||
override func loadView() {
|
||||
let root = UIView()
|
||||
root.backgroundColor = .black
|
||||
root.clipsToBounds = true
|
||||
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
imageView.backgroundColor = .black
|
||||
root.addSubview(imageView)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
imageView.leadingAnchor.constraint(equalTo: root.leadingAnchor),
|
||||
imageView.trailingAnchor.constraint(equalTo: root.trailingAnchor),
|
||||
imageView.topAnchor.constraint(equalTo: root.topAnchor),
|
||||
imageView.bottomAnchor.constraint(equalTo: root.bottomAnchor),
|
||||
])
|
||||
|
||||
self.view = root
|
||||
load(into: imageView)
|
||||
}
|
||||
|
||||
private func load(into imageView: UIImageView) {
|
||||
guard let url = imageURL else { return }
|
||||
// Use URLSession + URLCache so we cooperate with Expo Image's HTTP cache.
|
||||
let request = URLRequest(url: url, cachePolicy: .returnCacheDataElseLoad, timeoutInterval: 10)
|
||||
if let cached = URLCache.shared.cachedResponse(for: request),
|
||||
let image = UIImage(data: cached.data) {
|
||||
imageView.image = image
|
||||
return
|
||||
}
|
||||
URLSession.shared.dataTask(with: request) { [weak imageView] data, _, _ in
|
||||
guard let data = data, let image = UIImage(data: data) else { return }
|
||||
DispatchQueue.main.async { imageView?.image = image }
|
||||
}.resume()
|
||||
}
|
||||
|
||||
/// Caps the preview to a comfortable size within the current key window.
|
||||
private static func sizeForAspect(_ aspect: CGFloat) -> CGSize {
|
||||
let screen = UIScreen.main.bounds
|
||||
let maxW = screen.width - 32
|
||||
let maxH = screen.height * 0.7
|
||||
var w = maxW
|
||||
var h = w / aspect
|
||||
if h > maxH {
|
||||
h = maxH
|
||||
w = h * aspect
|
||||
}
|
||||
return CGSize(width: w, height: h)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user