import SwiftUI struct InteractiveImageView: View { let image: Image let contextMenu: MenuItems @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 { Group { image .resizable() .scaledToFit() .contextMenu { if currentScale == 1 { contextMenu } } } .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: (currentScale > 1 ? DragGesture(minimumDistance: 0) : nil) .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 = currentScale == 1 ? 2 : 1 previousScale = currentScale 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"), contextMenu: EmptyView() ) }