#if os(iOS) import SwiftUI import UIKit struct NativeInteractiveImageScrollView: UIViewRepresentable { @Binding var isZoomed: Bool let contentIdentifier: String let minimumZoomScale: CGFloat let maximumZoomScale: CGFloat let doubleTapZoomScale: CGFloat let content: Content init( isZoomed: Binding, contentIdentifier: String, minimumZoomScale: CGFloat, maximumZoomScale: CGFloat, doubleTapZoomScale: CGFloat, @ViewBuilder content: () -> Content ) { _isZoomed = isZoomed self.contentIdentifier = contentIdentifier self.minimumZoomScale = minimumZoomScale self.maximumZoomScale = maximumZoomScale self.doubleTapZoomScale = doubleTapZoomScale self.content = content() } func makeUIView(context: Context) -> UIScrollView { let scrollView = UIScrollView() scrollView.delegate = context.coordinator scrollView.minimumZoomScale = minimumZoomScale scrollView.maximumZoomScale = maximumZoomScale scrollView.zoomScale = minimumZoomScale scrollView.bouncesZoom = true scrollView.showsHorizontalScrollIndicator = false scrollView.showsVerticalScrollIndicator = false scrollView.contentInsetAdjustmentBehavior = .never scrollView.decelerationRate = .fast let hostingController = UIHostingController(rootView: content) hostingController.view.backgroundColor = .clear hostingController.view.translatesAutoresizingMaskIntoConstraints = false scrollView.addSubview(hostingController.view) NSLayoutConstraint.activate([ hostingController.view.leadingAnchor.constraint( equalTo: scrollView.contentLayoutGuide.leadingAnchor ), hostingController.view.trailingAnchor.constraint( equalTo: scrollView.contentLayoutGuide.trailingAnchor ), hostingController.view.topAnchor.constraint( equalTo: scrollView.contentLayoutGuide.topAnchor ), hostingController.view.bottomAnchor.constraint( equalTo: scrollView.contentLayoutGuide.bottomAnchor ), hostingController.view.widthAnchor.constraint( equalTo: scrollView.frameLayoutGuide.widthAnchor ), hostingController.view.heightAnchor.constraint( equalTo: scrollView.frameLayoutGuide.heightAnchor ), ]) let doubleTapGestureRecognizer = UITapGestureRecognizer( target: context.coordinator, action: #selector(Coordinator.handleDoubleTapGesture(_:)) ) doubleTapGestureRecognizer.numberOfTapsRequired = 2 scrollView.addGestureRecognizer(doubleTapGestureRecognizer) context.coordinator.hostingController = hostingController context.coordinator.hostedView = hostingController.view context.coordinator.latestContentIdentifier = contentIdentifier context.coordinator.updateZoomState(for: scrollView) context.coordinator.recenterContent(in: scrollView) return scrollView } func updateUIView(_ scrollView: UIScrollView, context: Context) { context.coordinator.parent = self context.coordinator.hostingController?.rootView = content if context.coordinator.latestContentIdentifier != contentIdentifier { context.coordinator.latestContentIdentifier = contentIdentifier scrollView.setZoomScale(minimumZoomScale, animated: false) scrollView.contentOffset = .zero } context.coordinator.recenterContent(in: scrollView) context.coordinator.updateZoomState(for: scrollView) } func makeCoordinator() -> Coordinator { Coordinator(parent: self) } final class Coordinator: NSObject, UIScrollViewDelegate { var parent: NativeInteractiveImageScrollView var hostingController: UIHostingController? weak var hostedView: UIView? var latestContentIdentifier: String? init(parent: NativeInteractiveImageScrollView) { self.parent = parent } func viewForZooming(in _: UIScrollView) -> UIView? { hostedView } func scrollViewDidZoom(_ scrollView: UIScrollView) { recenterContent(in: scrollView) updateZoomState(for: scrollView) } @objc func handleDoubleTapGesture(_ recognizer: UITapGestureRecognizer) { guard let scrollView = recognizer.view as? UIScrollView else { return } let minimumZoomScale = scrollView.minimumZoomScale let maximumZoomScale = scrollView.maximumZoomScale let targetZoomScale = min(parent.doubleTapZoomScale, maximumZoomScale) if scrollView.zoomScale > minimumZoomScale + 0.01 { scrollView.setZoomScale(minimumZoomScale, animated: true) return } guard let hostedView else { return } let tapLocation = recognizer.location(in: hostedView) let zoomRectangle = zoomRectangle( for: targetZoomScale, centeredAt: tapLocation, in: scrollView ) scrollView.zoom(to: zoomRectangle, animated: true) } func recenterContent(in scrollView: UIScrollView) { let horizontalInset = max((scrollView.bounds.width - scrollView.contentSize.width) / 2, 0) let verticalInset = max((scrollView.bounds.height - scrollView.contentSize.height) / 2, 0) scrollView.contentInset = UIEdgeInsets( top: verticalInset, left: horizontalInset, bottom: verticalInset, right: horizontalInset ) } func updateZoomState(for scrollView: UIScrollView) { let isCurrentlyZoomed = scrollView.zoomScale > scrollView.minimumZoomScale + 0.01 if parent.isZoomed != isCurrentlyZoomed { DispatchQueue.main.async { self.parent.isZoomed = isCurrentlyZoomed } } } private func zoomRectangle( for targetZoomScale: CGFloat, centeredAt tapLocation: CGPoint, in scrollView: UIScrollView ) -> CGRect { let rectangleWidth = scrollView.bounds.width / targetZoomScale let rectangleHeight = scrollView.bounds.height / targetZoomScale let horizontalOrigin = tapLocation.x - (rectangleWidth / 2) let verticalOrigin = tapLocation.y - (rectangleHeight / 2) return CGRect( x: horizontalOrigin, y: verticalOrigin, width: rectangleWidth, height: rectangleHeight ) } } } #endif