mirror of https://github.com/lukechilds/damus.git
OlegAba
2 years ago
committed by
William Casarin
4 changed files with 179 additions and 66 deletions
@ -0,0 +1,152 @@ |
|||
// |
|||
// 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) |
|||
} |
|||
} |
|||
} |
Loading…
Reference in new issue