// // ZoomableScrollView.swift // ComposeUI // // Created by Shadowfacts on 4/29/23. // import SwiftUI @available(iOS 16.0, *) struct ZoomableScrollView: UIViewControllerRepresentable { let content: Content init(@ViewBuilder content: () -> Content) { self.content = content() } func makeUIViewController(context: Context) -> Controller { return Controller(content: content) } func updateUIViewController(_ uiViewController: Controller, context: Context) { uiViewController.host.rootView = content } class Controller: UIViewController, UIScrollViewDelegate { let scrollView = UIScrollView() let host: UIHostingController private var lastIntrinsicSize: CGSize? private var contentViewTopConstraint: NSLayoutConstraint! private var contentViewLeadingConstraint: NSLayoutConstraint! private var hostBoundsObservation: NSKeyValueObservation? init(content: Content) { self.host = UIHostingController(rootView: content) super.init(nibName: nil, bundle: nil) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func viewDidLoad() { super.viewDidLoad() scrollView.delegate = self scrollView.bouncesZoom = true scrollView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(scrollView) host.sizingOptions = .intrinsicContentSize host.view.backgroundColor = .clear host.view.translatesAutoresizingMaskIntoConstraints = false addChild(host) scrollView.addSubview(host.view) host.didMove(toParent: self) contentViewLeadingConstraint = host.view.leadingAnchor.constraint(equalTo: scrollView.contentLayoutGuide.leadingAnchor) contentViewTopConstraint = host.view.topAnchor.constraint(equalTo: scrollView.contentLayoutGuide.topAnchor) NSLayoutConstraint.activate([ scrollView.frameLayoutGuide.leadingAnchor.constraint(equalTo: view.leadingAnchor), scrollView.frameLayoutGuide.trailingAnchor.constraint(equalTo: view.trailingAnchor), scrollView.frameLayoutGuide.topAnchor.constraint(equalTo: view.topAnchor), scrollView.frameLayoutGuide.bottomAnchor.constraint(equalTo: view.bottomAnchor), contentViewLeadingConstraint, contentViewTopConstraint, ]) } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() if !host.view.intrinsicContentSize.equalTo(.zero), host.view.intrinsicContentSize != lastIntrinsicSize { self.lastIntrinsicSize = host.view.intrinsicContentSize let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom let maxWidth = view.bounds.width - view.safeAreaInsets.left - view.safeAreaInsets.right let heightScale = maxHeight / host.view.intrinsicContentSize.height let widthScale = maxWidth / host.view.intrinsicContentSize.width let minScale = min(widthScale, heightScale) let maxScale = minScale >= 1 ? minScale + 2 : 2 scrollView.minimumZoomScale = minScale scrollView.maximumZoomScale = maxScale scrollView.zoomScale = minScale } centerImage() } func viewForZooming(in scrollView: UIScrollView) -> UIView? { return host.view } func scrollViewDidZoom(_ scrollView: UIScrollView) { centerImage() } func centerImage() { let yOffset = max(0, (view.bounds.size.height - host.view.bounds.height * scrollView.zoomScale) / 2) contentViewTopConstraint.constant = yOffset let xOffset = max(0, (view.bounds.size.width - host.view.bounds.width * scrollView.zoomScale) / 2) contentViewLeadingConstraint.constant = xOffset } } }