Add double tap gesture and fix bugs
Changlog-Added: Image double-tap gesture Closes: #397
This commit is contained in:
@@ -174,6 +174,7 @@
|
|||||||
7C45AE6D297352F90031D7BC /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6C297352F90031D7BC /* SVGKit */; };
|
7C45AE6D297352F90031D7BC /* SVGKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6C297352F90031D7BC /* SVGKit */; };
|
||||||
7C45AE6F297352F90031D7BC /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6E297352F90031D7BC /* SVGKitSwift */; };
|
7C45AE6F297352F90031D7BC /* SVGKitSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7C45AE6E297352F90031D7BC /* SVGKitSwift */; };
|
||||||
7C45AE71297353390031D7BC /* KFImageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7C45AE70297353390031D7BC /* KFImageModel.swift */; };
|
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 */; };
|
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9609F057296E220800069BF3 /* BannerImageView.swift */; };
|
||||||
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
|
BA693074295D649800ADDB87 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; };
|
||||||
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; };
|
||||||
@@ -724,6 +726,7 @@
|
|||||||
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */,
|
4CBCA92F297DB57F00EC6B2F /* WebsiteLink.swift */,
|
||||||
4CC7AAEC297F0B9E00430951 /* Highlight.swift */,
|
4CC7AAEC297F0B9E00430951 /* Highlight.swift */,
|
||||||
4CF0ABE22981BC7D00D66079 /* UserView.swift */,
|
4CF0ABE22981BC7D00D66079 /* UserView.swift */,
|
||||||
|
7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -996,6 +999,7 @@
|
|||||||
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
|
4C363AA828297703006E126D /* InsertSort.swift in Sources */,
|
||||||
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
|
4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */,
|
||||||
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
|
4C64987C286D03E000EAE2B3 /* DirectMessagesView.swift in Sources */,
|
||||||
|
7C902AE32981D55B002AB16E /* ZoomableScrollView.swift in Sources */,
|
||||||
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
|
4C363A8C28236B92006E126D /* PubkeyView.swift in Sources */,
|
||||||
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
|
4C5C7E68284ED36500A22DF5 /* SearchHomeModel.swift in Sources */,
|
||||||
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
4C75EFB728049D990006080F /* RelayPool.swift in Sources */,
|
||||||
|
|||||||
@@ -115,56 +115,6 @@ private struct ImageContainerView: View {
|
|||||||
// TODO: Update ImageCarousel with serializer and processor
|
// TODO: Update ImageCarousel with serializer and processor
|
||||||
// .serialize(by: imageModel.serializer)
|
// .serialize(by: imageModel.serializer)
|
||||||
// .setProcessor(imageModel.processor)
|
// .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 private var selectedIndex = 0
|
||||||
@State var showMenu = true
|
@State var showMenu = true
|
||||||
|
|
||||||
|
var safeAreaInsets: UIEdgeInsets? {
|
||||||
|
return UIApplication
|
||||||
|
.shared
|
||||||
|
.connectedScenes
|
||||||
|
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
|
||||||
|
.first { $0.isKeyWindow }?.safeAreaInsets
|
||||||
|
}
|
||||||
|
|
||||||
var navBarView: some View {
|
var navBarView: some View {
|
||||||
VStack {
|
VStack {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -222,22 +180,24 @@ struct ImageView: View {
|
|||||||
ZoomableScrollView {
|
ZoomableScrollView {
|
||||||
ImageContainerView(url: urls[index])
|
ImageContainerView(url: urls[index])
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.padding(.top, safeAreaInsets?.top)
|
||||||
|
.padding(.bottom, safeAreaInsets?.bottom)
|
||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
|
||||||
.tag(index)
|
|
||||||
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
|
.modifier(SwipeToDismissModifier(minDistance: 50, onDismiss: {
|
||||||
presentationMode.wrappedValue.dismiss()
|
presentationMode.wrappedValue.dismiss()
|
||||||
}))
|
}))
|
||||||
|
.ignoresSafeArea()
|
||||||
|
.tag(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ignoresSafeArea()
|
.ignoresSafeArea()
|
||||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||||
.onChange(of: selectedIndex, perform: { _ in
|
.gesture(TapGesture(count: 2).onEnded {
|
||||||
showMenu = true
|
// Prevents menu from hiding on double tap
|
||||||
})
|
})
|
||||||
.onTapGesture {
|
.gesture(TapGesture(count: 1).onEnded {
|
||||||
showMenu.toggle()
|
showMenu.toggle()
|
||||||
}
|
})
|
||||||
.overlay(
|
.overlay(
|
||||||
VStack {
|
VStack {
|
||||||
if showMenu {
|
if showMenu {
|
||||||
@@ -250,14 +210,7 @@ struct ImageView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.easeInOut, value: showMenu)
|
.animation(.easeInOut, value: showMenu)
|
||||||
.padding(
|
.padding(.bottom, safeAreaInsets?.bottom)
|
||||||
.bottom,
|
|
||||||
UIApplication
|
|
||||||
.shared
|
|
||||||
.connectedScenes
|
|
||||||
.flatMap { ($0 as? UIWindowScene)?.windows ?? [] }
|
|
||||||
.first { $0.isKeyWindow }?.safeAreaInsets.bottom
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
152
damus/Components/ZoomableScrollView.swift
Normal file
152
damus/Components/ZoomableScrollView.swift
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,13 +11,17 @@ struct SwipeToDismissModifier: ViewModifier {
|
|||||||
let minDistance: CGFloat?
|
let minDistance: CGFloat?
|
||||||
var onDismiss: () -> Void
|
var onDismiss: () -> Void
|
||||||
@State private var offset: CGSize = .zero
|
@State private var offset: CGSize = .zero
|
||||||
|
@GestureState private var viewOffset: CGSize = .zero
|
||||||
|
|
||||||
func body(content: Content) -> some View {
|
func body(content: Content) -> some View {
|
||||||
content
|
content
|
||||||
.offset(y: offset.height)
|
.offset(y: viewOffset.height)
|
||||||
.animation(.interactiveSpring(), value: offset)
|
.animation(.interactiveSpring(), value: viewOffset)
|
||||||
.simultaneousGesture(
|
.simultaneousGesture(
|
||||||
DragGesture(minimumDistance: minDistance ?? 10)
|
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
|
.onChanged { gesture in
|
||||||
if gesture.translation.width < 50 {
|
if gesture.translation.width < 50 {
|
||||||
offset = gesture.translation
|
offset = gesture.translation
|
||||||
|
|||||||
Reference in New Issue
Block a user