diff --git a/Sources/ArtemisMarkdown/ArtemisImageProvider.swift b/Sources/ArtemisMarkdown/ArtemisImageProvider.swift index c96c457..50b1241 100644 --- a/Sources/ArtemisMarkdown/ArtemisImageProvider.swift +++ b/Sources/ArtemisMarkdown/ArtemisImageProvider.swift @@ -20,9 +20,79 @@ struct ArtemisImageProvider: ImageProvider { if url?.absoluteString.contains("local://") == true { Self.assetProvider.makeImage(url: url) } else { - ArtemisAsyncImage(imageURL: url) {} - .scaledToFit() - .frame(maxWidth: .infinity, maxHeight: 400, alignment: .leading) + if #available(iOS 18.0, *) { + ImagePreview(url: url) + } else { + ArtemisAsyncImage(imageURL: url) {} + .scaledToFit() + .frame(maxWidth: .infinity, maxHeight: 400, alignment: .leading) + } + } + } +} + +@available(iOS 18.0, *) +private struct ImagePreview: View { + @Namespace private var namespace + @Environment(\.imagePreviewsEnabled) private var enabled + let url: URL? + private let id = UUID() + @State private var showPreview = false + @State private var image: UIImage? + + var body: some View { + ArtemisAsyncImage(imageURL: url, onSuccess: { result in + image = result.image + }) { + } + .scaledToFit() + .frame(maxWidth: .infinity, maxHeight: 400, alignment: .leading) + .matchedTransitionSource(id: id, in: namespace) + .modifier(ConditionalTapModifier(enabled: enabled) { + showPreview = true + }) + .navigationDestination(isPresented: $showPreview) { + GeometryReader { [image] proxy in + if let image { + ZoomableImagePreview(image: image) + .frame(width: proxy.size.width, height: proxy.size.height) + } else { + ArtemisAsyncImage(imageURL: url) {} + .scaledToFit() + } + } + .ignoresSafeArea() + .navigationTransition(.zoom(sourceID: id, in: namespace)) + } + } + + // Only add the gesture if needed, otherwise this may override other tap gestures + private struct ConditionalTapModifier: ViewModifier { + let enabled: Bool + let action: () -> Void + + func body(content: Content) -> some View { + if enabled { + content + .onTapGesture(perform: action) + } else { + content + } + } + } +} + +private enum ImagePreviewEnvironmentKey: EnvironmentKey { + static let defaultValue = false +} + +public extension EnvironmentValues { + var imagePreviewsEnabled: Bool { + get { + self[ImagePreviewEnvironmentKey.self] + } + set { + self[ImagePreviewEnvironmentKey.self] = newValue } } } diff --git a/Sources/ArtemisMarkdown/ZoomableImagePreview.swift b/Sources/ArtemisMarkdown/ZoomableImagePreview.swift new file mode 100644 index 0000000..515ff09 --- /dev/null +++ b/Sources/ArtemisMarkdown/ZoomableImagePreview.swift @@ -0,0 +1,29 @@ +// +// ZoomableImagePreview.swift +// ArtemisCore +// +// Created by Anian Schleyer on 14.11.24. +// + +import PDFKit +import SwiftUI + +// Adapted from https://stackoverflow.com/a/67577296 +struct ZoomableImagePreview: UIViewRepresentable { + let image: UIImage + + func makeUIView(context: Context) -> PDFView { + let view = PDFView() + view.document = PDFDocument() + guard let page = PDFPage(image: image) else { return view } + view.document?.insert(page, at: 0) + view.autoScales = true + DispatchQueue.main.async { + view.minScaleFactor = max(view.scaleFactorForSizeToFit * 0.8, 0) + view.maxScaleFactor = view.minScaleFactor + 5 + } + return view + } + + func updateUIView(_ uiView: PDFView, context: Context) {} +}