damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

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 }