Reputation: 238
Let's imagine, here is a ScrollView with some elements and I want to make some actions (e.g. changing of color) on long tap on these elements. But also I want to make possible to scroll this view.
Here is an example:
import SwiftUI
struct TextBox: View {
var text: String
var color: Color
@GestureState private var isLongPressure: Bool = false
var body: some View {
let longTap = LongPressGesture(minimumDuration: 0.3)
.updating($isLongPressure) { state, newState, transaction in
newState = state
transaction.animation = .easeOut(duration: 0.2)
}
Text(text)
.frame(width: 400, height: 200)
.background(isLongPressure ? .white : color)
.simultaneousGesture(longTap)
}
}
struct TestGestures: View {
var body: some View {
ScrollView {
TextBox(text: "Test 1", color: .red)
TextBox(text: "Test 2", color: .green)
TextBox(text: "Test 3", color: .blue)
TextBox(text: "Test 4", color: .red)
TextBox(text: "Test 5", color: .green)
TextBox(text: "Test 6", color: .blue)
}
}
}
struct TestGestures_Previews: PreviewProvider {
static var previews: some View {
TestGestures()
}
}
So, if I comment .simultaneousGesture(longTap)
– scrolling works, but if I uncomment it – scrolling stopped work.
P.S.: I've tried to add onTapGesture
before adding longTap and it doesn't help.
Upvotes: 4
Views: 1156
Reputation: 2873
This work launch print, but no change states with @GestureState, maybe change with @State
import SwiftUI
struct ScrollDetectGestures: View {
struct TextBox: View {
var text: String
var color: Color
@GestureState private var isLongPressure: Bool = false
@GestureState private var isTapped: Bool = false
var body: some View {
let longTap = LongPressGesture(minimumDuration: 0.3)
.updating($isLongPressure) { state, newState, transaction in
newState = state
transaction.animation = .easeOut(duration: 0.2)
}
.onEnded { _ in
print("Long press: " + text)
}
let tap = TapGesture()
.updating($isTapped) { state, newState, transaction in
newState = true
transaction.animation = .easeOut(duration: 0.2)
}
.onEnded { _ in
print("Tap press: " + text)
}
VStack {
Text(text)
Text("tap or long press")
.border(Color.black, width: isTapped ? 2 : 0)
}
.frame(width: 200, height: 100)
.background(color)
.simultaneousGesture(longTap)
.simultaneousGesture(tap)
}
}
var body: some View {
VStack {
ScrollView {
ForEach(ColorItem.allItems) { item in
TextBox(text: item.summary, color: item.color)
}
}
}
}
}
#Preview {
ScrollDetectGestures()
}
model data
struct ColorItem: Identifiable {
let id: UUID
let title: String
let summary: String
let color: Color
static let allItems: [ColorItem] = [
ColorItem(id: UUID(), title: "Red", summary: "This is red", color: .red),
ColorItem(id: UUID(), title: "Green", summary: "This is green", color: .green),
ColorItem(id: UUID(), title: "Blue", summary: "This is blue", color: .blue),
ColorItem(id: UUID(), title: "Yellow", summary: "This is yellow", color: .yellow),
ColorItem(id: UUID(), title: "Orange", summary: "This is orange", color: .orange),
ColorItem(id: UUID(), title: "Pink", summary: "This is pink", color: .pink),
ColorItem(id: UUID(), title: "Mint", summary: "This is Mint", color: .mint),
ColorItem(id: UUID(), title: "Teal", summary: "This is teal", color: .teal),
ColorItem(id: UUID(), title: "Cyan", summary: "This is cyan", color: .cyan),
ColorItem(id: UUID(), title: "Indigo", summary: "This is indigo", color: .indigo),
ColorItem(id: UUID(), title: "Purple", summary: "This is purple", color: .purple),
ColorItem(id: UUID(), title: "Gray", summary: "This is Gray", color: .gray),
ColorItem(id: UUID(), title: "Brown", summary: "This is Brown", color: .brown)
]
}
Upvotes: -1
Reputation: 1
I also struggled a lot with this topic. But found this really cool approach, which is working perfectly: https://danielsaidi.com/blog/2022/11/16/using-complex-gestures-in-a-scroll-view He did it with a buttonStyle:
struct ScrollViewGestureButtonStyle: ButtonStyle {
init(
pressAction: @escaping () -> Void,
doubleTapTimeoutout: TimeInterval,
doubleTapAction: @escaping () -> Void,
longPressTime: TimeInterval,
longPressAction: @escaping () -> Void,
endAction: @escaping () -> Void
) {
self.pressAction = pressAction
self.doubleTapTimeoutout = doubleTapTimeoutout
self.doubleTapAction = doubleTapAction
self.longPressTime = longPressTime
self.longPressAction = longPressAction
self.endAction = endAction
}
private var doubleTapTimeoutout: TimeInterval
private var longPressTime: TimeInterval
private var pressAction: () -> Void
private var longPressAction: () -> Void
private var doubleTapAction: () -> Void
private var endAction: () -> Void
@State
var doubleTapDate = Date()
@State
var longPressDate = Date()
func makeBody(configuration: Configuration) -> some View {
configuration.label
.onChange(of: configuration.isPressed) { isPressed in
longPressDate = Date()
if isPressed {
pressAction()
doubleTapDate = tryTriggerDoubleTap() ? .distantPast : .now
tryTriggerLongPressAfterDelay(triggered: longPressDate)
} else {
endAction()
}
}
}
}
private extension ScrollViewGestureButtonStyle {
func tryTriggerDoubleTap() -> Bool {
let interval = Date().timeIntervalSince(doubleTapDate)
guard interval < doubleTapTimeoutout else { return false }
doubleTapAction()
return true
}
func tryTriggerLongPressAfterDelay(triggered date: Date) {
DispatchQueue.main.asyncAfter(deadline: .now() + longPressTime) {
guard date == longPressDate else { return }
longPressAction()
}
}
}
Upvotes: 0
Reputation: 331
Here is a version of @nickreps solution, but a bit shorter:
struct MyView : View {
var body: some View {
ScrollView { VStack { ForEach(0..<40) { _ in Item() } } }
}
}
struct Item : View {
var body: some View {
Color.yellow
.frame(height: 100)
.border(Color.black, width: 1)
.yieldTouches() // <- this solves gestures conflict
.gesture(
LongPressGesture(minimumDuration: 1, maximumDistance: 10)
.onEnded { _ in print("hello!") }
)
}
}
public extension View {
func yieldTouches() -> some View { modifier(YieldTouches()) }
}
private struct YieldTouches : ViewModifier {
@State private var disabled = false
func body(content: Content) -> some View {
content
.disabled(disabled)
.onTapGesture { onMain { disabled = true; onMain { disabled = false } } }
}
private func onMain(_ action: @escaping () -> Void) { DispatchQueue.main.async(execute: action) }
}
Upvotes: 1
Reputation: 1080
I was able to get it working by utilizing a button rather than a TextView. Although this does directly utilize the code you provided, you should be able to modify some pieces to have it meet your needs (I can help with this, if needed!)
import SwiftUI
struct ScrollTest: View {
let testData = [1]
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
AnimatedButtonView(color: .red, text: "Test 1")
AnimatedButtonView(color: .green, text: "Test 2")
AnimatedButtonView(color: .blue, text: "Test 3")
}
}
}
struct AnimatedButtonView: View {
@GestureState var isDetectingLongPress = false
let color: Color
let text: String
var body: some View {
ZStack {
RoundedRectangle(cornerRadius: 12.5, style: .continuous)
.fill(color)
.frame(width: UIScreen.main.bounds.width, height: 200)
.padding(25)
.scaleEffect(!isDetectingLongPress ? 1.0 : 0.875)
.brightness(!isDetectingLongPress ? 0.0 : -0.125)
.animation(.easeInOut(duration: 0.125), value: isDetectingLongPress)
Text(text)
}
.delaysTouches(for: 0.01) {
//some code here, if needed
}
.gesture(
LongPressGesture(minimumDuration: 3)
.updating($isDetectingLongPress) { currentState, gestureState,
transaction in
gestureState = currentState
transaction.animation = Animation.easeIn(duration: 2.0)
}
.onEnded { finished in
print("gesture ended")
})
}
}
extension View {
func delaysTouches(for duration: TimeInterval = 0.25, onTap action: @escaping () -> Void = {}) -> some View {
modifier(DelaysTouches(duration: duration, action: action))
}
}
fileprivate struct DelaysTouches: ViewModifier {
@State private var disabled = false
@State private var touchDownDate: Date? = nil
var duration: TimeInterval
var action: () -> Void
func body(content: Content) -> some View {
Button(action: action) {
content
}
.buttonStyle(DelaysTouchesButtonStyle(disabled: $disabled, duration: duration, touchDownDate: $touchDownDate))
.disabled(disabled)
}
}
fileprivate struct DelaysTouchesButtonStyle: ButtonStyle {
@Binding var disabled: Bool
var duration: TimeInterval
@Binding var touchDownDate: Date?
func makeBody(configuration: Configuration) -> some View {
configuration.label
.onChange(of: configuration.isPressed, perform: handleIsPressed)
}
private func handleIsPressed(isPressed: Bool) {
if isPressed {
let date = Date()
touchDownDate = date
DispatchQueue.main.asyncAfter(deadline: .now() + max(duration, 0)) {
if date == touchDownDate {
disabled = true
DispatchQueue.main.async {
disabled = false
}
}
}
} else {
touchDownDate = nil
disabled = false
}
}
}
Upvotes: 2
Reputation: 800
I'm not sure I understand the exact context, but you could add a condition so your LongPressGesture
only triggers an action when gesture is not being used for scrolling.
let longTap = LongPressGesture(minimumDuration: 0.3)
.updating($isLongPressure) { value, state, transaction in
if value {
state = true
transaction.animation = .easeOut(duration: 0.2)
}
}
Upvotes: 0