commit 5de745fb19ecacaa57c4e501be3a477722ae5347
parent 1baae90bebb9bb1fab8f05377aa0e480f1cdf0f1
Author: OlegAba <mail@olegaba.com>
Date: Wed, 25 Jan 2023 16:25:55 -0500
Add double tap gesture and fix bugs
Changlog-Added: Image double-tap gesture
Closes: #397
Diffstat:
4 files changed, 179 insertions(+), 66 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -174,6 +174,7 @@
7C45AE6D297352F90031D7BC /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6C297352F90031D7BC /* SVGKit */; };
7C45AE6F297352F90031D7BC /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6E297352F90031D7BC /* SVGKitSwift */; };
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C45AE70297353390031D7BC /* KFImageModel.swift */; };
+ 7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */; };
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
@@ -423,6 +424,7 @@
647D9A8C2968520300A295DE /* SideMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SideMenuView.swift; sourceTree = "<group>"; };
64FBD06E296255C400D9D3B2 /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
7C45AE70297353390031D7BC /* KFImageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KFImageModel.swift; sourceTree = "<group>"; };
+ 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZoomableScrollView.swift; sourceTree = "<group>"; };
9609F057296E220800069BF3 /* BannerImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerImageView.swift; sourceTree = "<group>"; };
BA693073295D649800ADDB87 /* UserSettingsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserSettingsStore.swift; sourceTree = "<group>"; };
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
@@ -724,6 +726,7 @@
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */,
4CC7AAEC297F0B9E00430951 /* Highlight.swift */,
4CF0ABE22981BC7D00D66079 /* UserView.swift */,
+ 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -996,6 +999,7 @@
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
+ 7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
diff --git a/damus/Components/ImageCarousel.swift b/damus/Components/ImageCarousel.swift
@@ -115,56 +115,6 @@ private struct ImageContainerView: View {
// TODO: Update ImageCarousel with serializer and processor
// .serialize(by: imageModel.serializer)
// .setProcessor(imageModel.processor)
-
- }
-}
-
-struct ZoomableScrollView<Content: View>: UIViewRepresentable {
-
- private var content: Content
-
- init(@ViewBuilder content: () -> Content) {
- self.content = content()
- }
-
- func makeUIView(context: Context) -> UIScrollView {
- let scrollView = UIScrollView()
- 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))
- }
-
- 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
- }
}
}
@@ -177,6 +127,14 @@ struct ImageView: View {
@State private var selectedIndex = 0
@State var showMenu = true
+ var safeAreaInsets: UIEdgeInsets? {
+ return UIApplication
+ .shared
+ .connectedScenes
+ .flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
+ .first { $0.isKeyWindow }?.safeAreaInsets
+ }
+
var navBarView: some View {
VStack {
HStack {
@@ -222,22 +180,24 @@ struct ImageView: View {
ZoomableScrollView {
ImageContainerView(url: urls[index])
.aspectRatio(contentMode: .fit)
+ .padding(.top, safeAreaInsets?.top)
+ .padding(.bottom, safeAreaInsets?.bottom)
}
- .ignoresSafeArea()
- .tag(index)
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
presentationMode.wrappedValue.dismiss()
}))
+ .ignoresSafeArea()
+ .tag(index)
}
}
.ignoresSafeArea()
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
- .onChange(of: selectedIndex, perform: { _ in
- showMenu = true
+ .gesture(TapGesture(count: 2).onEnded {
+ // Prevents menu from hiding on double tap
})
- .onTapGesture {
+ .gesture(TapGesture(count: 1).onEnded {
showMenu.toggle()
- }
+ })
.overlay(
VStack {
if showMenu {
@@ -250,14 +210,7 @@ struct ImageView: View {
}
}
.animation(.easeInOut, value: showMenu)
- .padding(
- .bottom,
- UIApplication
- .shared
- .connectedScenes
- .flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
- .first { $0.isKeyWindow }?.safeAreaInsets.bottom
- )
+ .padding(.bottom, safeAreaInsets?.bottom)
)
}
}
diff --git a/damus/Components/ZoomableScrollView.swift b/damus/Components/ZoomableScrollView.swift
@@ -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)
+ }
+ }
+}
diff --git a/damus/Modifiers/SwipeToDismiss.swift b/damus/Modifiers/SwipeToDismiss.swift
@@ -11,13 +11,17 @@ struct SwipeToDismissModifier: ViewModifier {
let minDistance: CGFloat?
var onDismiss: () -> Void
@State private var offset: CGSize = .zero
+ @GestureState private var viewOffset: CGSize = .zero
func body(content: Content) -> some View {
content
- .offset(y: offset.height)
- .animation(.interactiveSpring(), value: offset)
+ .offset(y: viewOffset.height)
+ .animation(.interactiveSpring(), value: viewOffset)
.simultaneousGesture(
DragGesture(minimumDistance: minDistance ?? 10)
+ .updating($viewOffset, body: { value, gestureState, transaction in
+ gestureState = CGSize(width: value.location.x - value.startLocation.x, height: value.location.y - value.startLocation.y)
+ })
.onChanged { gesture in
if gesture.translation.width < 50 {
offset = gesture.translation