From 570a49d5c8ab326f3207e95538559b2a6f03fe59 Mon Sep 17 00:00:00 2001 From: Fuwn Date: Sat, 22 Feb 2025 06:35:52 -0800 Subject: feat: Development commit --- Sora/Other/AsyncImageWithPreview.swift | 168 +++++++++------------------------ Sora/Views/ZoomableImageView.swift | 89 +++++++++++++++++ 2 files changed, 133 insertions(+), 124 deletions(-) create mode 100644 Sora/Views/ZoomableImageView.swift diff --git a/Sora/Other/AsyncImageWithPreview.swift b/Sora/Other/AsyncImageWithPreview.swift index 2e29052..9cae976 100644 --- a/Sora/Other/AsyncImageWithPreview.swift +++ b/Sora/Other/AsyncImageWithPreview.swift @@ -12,138 +12,58 @@ struct AsyncImageWithPreview: View { @State private var finalOffset: CGSize = .zero 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) + AsyncImage(url: url) { image in + ZoomableImageView(image: image) + .onAppear { + loadingState = finalLoadingState + } + .contextMenu { + #if os(iOS) + Button { + guard let url else { return } - 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 } + 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") + UIImageWriteToSavedPhotosAlbum(uiImage, nil, nil, nil) } - #endif + .resume() + } label: { + Label("Save Image", systemImage: "square.and.arrow.down") + } + #endif - #if os(iOS) - Button { - let activityViewController = UIActivityViewController( - activityItems: [url ?? URL(string: "")!], applicationActivities: nil - ) + #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 + 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") - } + 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 - } - } + } + } placeholder: { + placeholder() + .onAppear { + loadingState = .loadingPreview + } } } diff --git a/Sora/Views/ZoomableImageView.swift b/Sora/Views/ZoomableImageView.swift new file mode 100644 index 0000000..8304c6e --- /dev/null +++ b/Sora/Views/ZoomableImageView.swift @@ -0,0 +1,89 @@ +import SwiftUI + +struct ZoomableImageView: View { + let image: Image + @State private var screenWidth = 0.0 + @State private var screenHeight = 0.0 + @State private var currentScale = 1.0 + @State private var previousScale = 0.0 + @State private var currentOffset: CGSize = .zero + @State private var previousOffset: CGSize = .zero + + var body: some View { + GeometryReader { geometry in + VStack { + image + .resizable() + .scaledToFit() + .scaleEffect(currentScale) + .offset(currentOffset) + .frame(width: screenWidth, height: screenHeight) + .clipped() + .gesture( + MagnifyGesture() + .onChanged { gesture in + withAnimation(.interactiveSpring()) { + currentScale = + previousScale + gesture.magnification - (previousScale == 0 ? 0 : 1) + } + } + .onEnded { _ in + previousScale = currentScale + } + .simultaneously( + with: DragGesture(minimumDistance: 0) + .onChanged { gesture in + withAnimation(.interactiveSpring()) { + var newOffset: CGSize = .zero + let offset = gesture.translation + + newOffset.width = offset.width + previousOffset.width + newOffset.height = offset.height + previousOffset.height + + currentOffset = clampOffset(offset: newOffset) + } + } + .onEnded { _ in + previousOffset = currentOffset + } + ) + ) + .highPriorityGesture( + TapGesture(count: 2) + .onEnded { + withAnimation { + currentScale = 1.0 + previousScale = 0 + currentOffset = .zero + previousOffset = .zero + } + } + ) + } + .onAppear { + screenWidth = geometry.size.width + screenHeight = geometry.size.height + } + } + } + + private func clampOffset(offset: CGSize = .zero) -> CGSize { + var newOffset = offset + + if currentScale > 1 { + let maxX = ((screenWidth * currentScale) - screenWidth) / 2 + let maxY = ((screenHeight * currentScale) - screenHeight) / 2 + + newOffset.width = min(max(-maxX, newOffset.width), maxX) + newOffset.height = min(max(-maxY, newOffset.height), maxY) + } else { + newOffset = .zero + } + + return newOffset + } +} + +#Preview { + ZoomableImageView(image: Image(systemName: "photo")) +} -- cgit v1.2.3