|
// |
|
// ZoomableImageView.swift |
|
// |
|
// Created by Don Park on 4/30/25. |
|
// |
|
|
|
import SwiftUI |
|
|
|
struct ZoomableContainer<Content: View>: View { |
|
let content: Content |
|
let contentSize: () -> CGSize |
|
|
|
@State private var scale: CGFloat = 1.0 |
|
@State private var offset: CGSize = .zero |
|
|
|
@State private var gestureScale: CGFloat = 1.0 |
|
@State private var gestureOffset: CGSize = .zero |
|
|
|
private let minScale: CGFloat = 1.0 |
|
private var maxScale: CGFloat { 3.0 } |
|
|
|
init(@ViewBuilder content: () -> Content, contentSize: @escaping () -> CGSize) { |
|
self.content = content() |
|
self.contentSize = contentSize |
|
} |
|
|
|
var body: some View { |
|
GeometryReader { geo in |
|
let viewSize = geo.size |
|
let contentSize = contentSize() |
|
let fitScale = min(viewSize.width / contentSize.width, viewSize.height / contentSize.height) |
|
let minAllowedScale = fitScale |
|
let maxAllowedScale = fitScale * maxScale |
|
|
|
content |
|
.frame(width: contentSize.width, height: contentSize.height) |
|
.scaleEffect(scale * gestureScale, anchor: .center) |
|
.offset(x: offset.width + gestureOffset.width, y: offset.height + gestureOffset.height) |
|
.gesture( |
|
SimultaneousGesture( |
|
MagnificationGesture() |
|
.onChanged { value in |
|
gestureScale = value |
|
} |
|
.onEnded { value in |
|
let newScale = scale * value |
|
let clampedScale = min(max(newScale, minAllowedScale), maxAllowedScale) |
|
withAnimation(.spring()) { |
|
scale = clampedScale |
|
gestureScale = 1.0 |
|
} |
|
}, |
|
DragGesture() |
|
.onChanged { value in |
|
gestureOffset = value.translation |
|
} |
|
.onEnded { value in |
|
let totalScale = scale * gestureScale |
|
let imageDisplaySize = CGSize(width: contentSize.width * totalScale, height: contentSize.height * totalScale) |
|
let maxOffsetX = max(0, (imageDisplaySize.width - viewSize.width) / 2) |
|
let maxOffsetY = max(0, (imageDisplaySize.height - viewSize.height) / 2) |
|
var newOffset = CGSize( |
|
width: offset.width + value.translation.width, |
|
height: offset.height + value.translation.height |
|
) |
|
// Animate back if out of bounds |
|
newOffset.width = min(max(newOffset.width, -maxOffsetX), maxOffsetX) |
|
newOffset.height = min(max(newOffset.height, -maxOffsetY), maxOffsetY) |
|
withAnimation(.spring()) { |
|
offset = newOffset |
|
gestureOffset = .zero |
|
} |
|
} |
|
) |
|
) |
|
.onChange(of: scale) { |
|
// Reset offset if scale changes and image shrinks |
|
withAnimation(.spring()) { |
|
offset = .zero |
|
} |
|
} |
|
.frame(width: viewSize.width, height: viewSize.height) |
|
.background(Color.black.opacity(0.8)) |
|
} |
|
} |
|
} |