// // ZoomableScrollView.swift // damus // // Created by Oleg Abalonski on 1/25/23. // import SwiftUI struct ZoomableScrollView: UIViewRepresentable { private var content: Content init(@ViewBuilder content: () -> Content) { self.content = content() } func makeUIView(context: Context) -> UIScrollView { let scrollView = GesturedScrollView() scrollView.delegate = context.coordinator scrollView.maximumZoomScale = 20 scrollView.minimumZoomScale = 1 scrollView.bouncesZoom = true scrollView.showsVerticalScrollIndicator = false scrollView.showsHorizontalScrollIndicator = false let hostedView = context.coordinator.hostingController.view! hostedView.translatesAutoresizingMaskIntoConstraints = true hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight] hostedView.frame = scrollView.bounds hostedView.backgroundColor = .clear scrollView.addSubview(hostedView) return scrollView } func makeCoordinator() -> Coordinator { return Coordinator(hostingController: UIHostingController(rootView: self.content, ignoreSafeArea: true)) } func updateUIView(_ uiView: UIScrollView, context: Context) { context.coordinator.hostingController.rootView = self.content assert(context.coordinator.hostingController.view.superview == uiView) } class Coordinator: NSObject, UIScrollViewDelegate { var hostingController: UIHostingController init(hostingController: UIHostingController) { self.hostingController = hostingController } func viewForZooming(in scrollView: UIScrollView) -> UIView? { return hostingController.view } func scrollViewDidZoom(_ scrollView: UIScrollView) { let viewSize = hostingController.view.frame.size guard let imageSize = scrollView.subviews[0].subviews.last?.frame.size else { return } if scrollView.zoomScale > 1 { let ratioW = viewSize.width / imageSize.width let ratioH = viewSize.height / imageSize.height let ratio = ratioW < ratioH ? ratioW:ratioH let newWidth = imageSize.width * ratio let newHeight = imageSize.height * ratio let left = 0.5 * (newWidth * scrollView.zoomScale > viewSize.width ? (newWidth - viewSize.width) : (scrollView.frame.width - scrollView.contentSize.width)) let top = 0.5 * (newHeight * scrollView.zoomScale > viewSize.height ? (newHeight - viewSize.height) : (scrollView.frame.height - scrollView.contentSize.height)) scrollView.contentInset = UIEdgeInsets(top: top, left: left, bottom: top, right: left) } else { scrollView.contentInset = .zero } } } } fileprivate class GesturedScrollView: UIScrollView, UIGestureRecognizerDelegate { let doubleTapGesture: UITapGestureRecognizer override init(frame: CGRect) { doubleTapGesture = UITapGestureRecognizer() super.init(frame: frame) doubleTapGesture.addTarget(self, action: #selector(handleDoubleTap)) doubleTapGesture.numberOfTapsRequired = 2 addGestureRecognizer(doubleTapGesture) doubleTapGesture.delegate = self } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) { if self.zoomScale == 1 { let pointInView = gesture.location(in: self.subviews.first) let newZoomScale = self.maximumZoomScale / 4.0 let scrollViewSize = self.bounds.size let width = scrollViewSize.width / newZoomScale let height = scrollViewSize.height / newZoomScale let originX = pointInView.x - (width / 2.0) let originY = pointInView.y - (height / 2.0) let zoomRect = CGRect(x: originX, y: originY, width: width, height: height) self.zoom(to: zoomRect, animated: true) } else { self.setZoomScale(self.minimumZoomScale, animated: true) } } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { return gestureRecognizer == doubleTapGesture } } fileprivate extension UIHostingController { convenience init(rootView: Content, ignoreSafeArea: Bool) { self.init(rootView: rootView) if ignoreSafeArea { disableSafeArea() } } func disableSafeArea() { guard let viewClass = object_getClass(view) else { return } let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea") if let viewSubclass = NSClassFromString(viewSubclassName) { object_setClass(view, viewSubclass) } else { guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return } guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return } if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) { let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in return .zero } class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method)) } objc_registerClassPair(viewSubclass) object_setClass(view, viewSubclass) } } }