ZoomableScrollView.swift (5903B)
1 // 2 // ZoomableScrollView.swift 3 // damus 4 // 5 // Created by Oleg Abalonski on 1/25/23. 6 // 7 8 import SwiftUI 9 10 struct ZoomableScrollView<Content: View>: UIViewRepresentable { 11 12 private var content: Content 13 14 init(@ViewBuilder content: () -> Content) { 15 self.content = content() 16 } 17 18 func makeUIView(context: Context) -> UIScrollView { 19 let scrollView = GesturedScrollView() 20 scrollView.delegate = context.coordinator 21 scrollView.maximumZoomScale = 20 22 scrollView.minimumZoomScale = 1 23 scrollView.bouncesZoom = true 24 scrollView.showsVerticalScrollIndicator = false 25 scrollView.showsHorizontalScrollIndicator = false 26 27 let hostedView = context.coordinator.hostingController.view! 28 hostedView.translatesAutoresizingMaskIntoConstraints = true 29 hostedView.autoresizingMask = [.flexibleWidth, .flexibleHeight] 30 hostedView.frame = scrollView.bounds 31 hostedView.backgroundColor = .clear 32 scrollView.addSubview(hostedView) 33 34 return scrollView 35 } 36 37 func makeCoordinator() -> Coordinator { 38 return Coordinator(hostingController: UIHostingController(rootView: self.content, ignoreSafeArea: true)) 39 } 40 41 func updateUIView(_ uiView: UIScrollView, context: Context) { 42 context.coordinator.hostingController.rootView = self.content 43 assert(context.coordinator.hostingController.view.superview == uiView) 44 } 45 46 class Coordinator: NSObject, UIScrollViewDelegate { 47 var hostingController: UIHostingController<Content> 48 49 init(hostingController: UIHostingController<Content>) { 50 self.hostingController = hostingController 51 } 52 53 func viewForZooming(in scrollView: UIScrollView) -> UIView? { 54 return hostingController.view 55 } 56 57 func scrollViewDidZoom(_ scrollView: UIScrollView) { 58 let viewSize = hostingController.view.frame.size 59 guard let imageSize = scrollView.subviews[0].subviews.last?.frame.size else { return } 60 61 if scrollView.zoomScale > 1 { 62 63 let ratioW = viewSize.width / imageSize.width 64 let ratioH = viewSize.height / imageSize.height 65 66 let ratio = ratioW < ratioH ? ratioW:ratioH 67 68 let newWidth = imageSize.width * ratio 69 let newHeight = imageSize.height * ratio 70 71 let left = 0.5 * (newWidth * scrollView.zoomScale > viewSize.width ? (newWidth - viewSize.width) : (scrollView.frame.width - scrollView.contentSize.width)) 72 let top = 0.5 * (newHeight * scrollView.zoomScale > viewSize.height ? (newHeight - viewSize.height) : (scrollView.frame.height - scrollView.contentSize.height)) 73 74 scrollView.contentInset = UIEdgeInsets(top: top, left: left, bottom: top, right: left) 75 } else { 76 scrollView.contentInset = .zero 77 } 78 } 79 } 80 } 81 82 fileprivate class GesturedScrollView: UIScrollView, UIGestureRecognizerDelegate { 83 84 let doubleTapGesture: UITapGestureRecognizer 85 86 override init(frame: CGRect) { 87 doubleTapGesture = UITapGestureRecognizer() 88 super.init(frame: frame) 89 doubleTapGesture.addTarget(self, action: #selector(handleDoubleTap)) 90 doubleTapGesture.numberOfTapsRequired = 2 91 addGestureRecognizer(doubleTapGesture) 92 doubleTapGesture.delegate = self 93 } 94 95 required init?(coder: NSCoder) { 96 fatalError("init(coder:) has not been implemented") 97 } 98 99 @objc func handleDoubleTap(_ gesture: UITapGestureRecognizer) { 100 if self.zoomScale == 1 { 101 let pointInView = gesture.location(in: self.subviews.first) 102 let newZoomScale = self.maximumZoomScale / 4.0 103 let scrollViewSize = self.bounds.size 104 let width = scrollViewSize.width / newZoomScale 105 let height = scrollViewSize.height / newZoomScale 106 let originX = pointInView.x - (width / 2.0) 107 let originY = pointInView.y - (height / 2.0) 108 let zoomRect = CGRect(x: originX, y: originY, width: width, height: height) 109 self.zoom(to: zoomRect, animated: true) 110 } else { 111 self.setZoomScale(self.minimumZoomScale, animated: true) 112 } 113 } 114 115 func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { 116 return gestureRecognizer == doubleTapGesture 117 } 118 } 119 120 fileprivate extension UIHostingController { 121 122 convenience init(rootView: Content, ignoreSafeArea: Bool) { 123 self.init(rootView: rootView) 124 125 if ignoreSafeArea { 126 disableSafeArea() 127 } 128 } 129 130 func disableSafeArea() { 131 guard let viewClass = object_getClass(view) else { return } 132 133 let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea") 134 if let viewSubclass = NSClassFromString(viewSubclassName) { 135 object_setClass(view, viewSubclass) 136 } 137 else { 138 guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return } 139 guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return } 140 141 if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) { 142 let safeAreaInsets: @convention(block) (AnyObject) -> UIEdgeInsets = { _ in 143 return .zero 144 } 145 class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method)) 146 } 147 148 objc_registerClassPair(viewSubclass) 149 object_setClass(view, viewSubclass) 150 } 151 } 152 }