153 lines
5.8 KiB
Swift
153 lines
5.8 KiB
Swift
//
|
|
// ZoomableScrollView.swift
|
|
// damus
|
|
//
|
|
// Created by Oleg Abalonski on 1/25/23.
|
|
//
|
|
|
|
import SwiftUI
|
|
|
|
struct ZoomableScrollView<Content: View>: 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<Content>
|
|
|
|
init(hostingController: UIHostingController<Content>) {
|
|
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)
|
|
}
|
|
}
|
|
}
|