import SwiftUI struct InteractiveImageView: 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 = 1.0 @State private var currentOffset: CGSize = .zero @State private var previousOffset: CGSize = .zero @State private var zoomAnchor: UnitPoint = .center var body: some View { image .resizable() .scaledToFit() .scaleEffect(currentScale, anchor: zoomAnchor) .offset(currentOffset) .frame(maxWidth: .infinity, maxHeight: .infinity) .clipped() .background( GeometryReader { geometry in Color.clear .onAppear { screenWidth = geometry.size.width screenHeight = geometry.size.height } } ) .gesture( MagnifyGesture() .onChanged { gesture in withAnimation(.interactiveSpring()) { if previousScale == 1 { zoomAnchor = gesture.startAnchor } currentScale = max(previousScale * gesture.magnification, 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 previousScale = 1 currentOffset = .zero previousOffset = .zero zoomAnchor = .center } } ) } 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 { InteractiveImageView(image: Image(systemName: "photo")) }