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