d4fd15dc20
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
73 lines
2.5 KiB
Swift
73 lines
2.5 KiB
Swift
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)
|
|
}
|
|
}
|