import SwiftUI struct AsyncImageWithPreview: View { var url: URL? @Binding var loadingState: PostLoadingState var finalLoadingState: PostLoadingState var postURL: URL? let placeholder: () -> Placeholder @State private var currentScale: CGFloat = 1.0 @State private var finalScale: CGFloat = 1.0 @State private var currentOffset: CGSize = .zero @State private var finalOffset: CGSize = .zero init( url: URL?, loadingStage: Binding, finalLoadingState: PostLoadingState = .loadingFile, postURL: URL? = nil, @ViewBuilder placeholder: @escaping () -> Placeholder = { GeometryReader { geometry in ProgressView() .frame(width: geometry.size.width, height: geometry.size.height) .position(x: geometry.size.width / 2, y: geometry.size.height / 2) .padding() } } ) { self.url = url _loadingState = loadingStage self.finalLoadingState = finalLoadingState self.postURL = postURL self.placeholder = placeholder } var body: some View { GeometryReader { geometry in AsyncImage(url: url) { image in image .resizable() .scaledToFit() .onAppear { loadingState = finalLoadingState } .scaleEffect(finalScale * currentScale) .offset( x: finalOffset.width + currentOffset.width, y: finalOffset.height + currentOffset.height ) .frame(width: geometry.size.width, height: geometry.size.height) .position(x: geometry.size.width / 2, y: geometry.size.height / 2) .gesture( DragGesture() .onChanged { value in let translation = value.translation let newOffset = CGSize( width: finalOffset.width + translation.width, height: finalOffset.height + translation.height ) let scale = finalScale * currentScale let imageWidth = geometry.size.width * scale let imageHeight = geometry.size.height * scale let maxX = max((imageWidth - geometry.size.width) / 2, 0) let maxY = max((imageHeight - geometry.size.height) / 2, 0) let clampedX = min(max(newOffset.width, -maxX), maxX) let clampedY = min(max(newOffset.height, -maxY), maxY) currentOffset = CGSize( width: clampedX - finalOffset.width, height: clampedY - finalOffset.height ) } .onEnded { value in let translation = value.translation var newOffset = CGSize( width: finalOffset.width + translation.width, height: finalOffset.height + translation.height ) let scale = finalScale * currentScale let imageWidth = geometry.size.width * scale let imageHeight = geometry.size.height * scale let maxX = max((imageWidth - geometry.size.width) / 2, 0) let maxY = max((imageHeight - geometry.size.height) / 2, 0) newOffset.width = min(max(newOffset.width, -maxX), maxX) newOffset.height = min(max(newOffset.height, -maxY), maxY) finalOffset = newOffset currentOffset = .zero } ) .simultaneousGesture( MagnificationGesture() .onChanged { value in currentScale = value } .onEnded { _ in finalScale *= currentScale currentScale = 1.0 let scale = finalScale let imageWidth = geometry.size.width * scale let imageHeight = geometry.size.height * scale let maxX = max((imageWidth - geometry.size.width) / 2, 0) let maxY = max((imageHeight - geometry.size.height) / 2, 0) finalOffset.width = min(max(finalOffset.width, -maxX), maxX) finalOffset.height = min(max(finalOffset.height, -maxY), maxY) } ) .highPriorityGesture( TapGesture(count: 2) .onEnded { withAnimation { finalScale = 1.0 currentScale = 1.0 finalOffset = .zero currentOffset = .zero } } ) .contextMenu { #if os(iOS) Button { guard let url else { return } URLSession.shared.dataTask(with: url) { data, _, _ in guard let data, let uiImage = UIImage(data: data) else { return } UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil) }.resume() } label: { Label("Save Image", systemImage: "square.and.arrow.down") } #endif #if os(iOS) Button { let activityViewController = UIActivityViewController( activityItems: [url ?? URL(string: "")!], applicationActivities: nil) UIApplication.shared.windows.first?.rootViewController?.present( activityViewController, animated: true) } label: { Label("Share Image", systemImage: "square.and.arrow.up") } #endif if let url = postURL { Button { #if os(iOS) UIApplication.shared.open(url) #else NSWorkspace.shared.open(url) #endif } label: { Label("Open in Safari", systemImage: "safari") } } } } placeholder: { placeholder() .onAppear { loadingState = .loadingPreview } } } } }