如何在SwiftUI中检测点击手势位置?

Epa*_*aga 5 swift swiftui

(对于SwiftUI,不是普通的UIKit)非常简单的示例代码,例如,在灰色背景上显示红色框:

struct ContentView : View {
    @State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(x:50,y:50)]
    var body: some View {
        return ZStack {
            Color.gray
                .tapAction {
                   // TODO: add an entry to self.points of the location of the tap
                }
            ForEach(self.points.identified(by: \.debugDescription)) {
                point in
                Color.red
                    .frame(width:50, height:50, alignment: .center)
                    .offset(CGSize(width: point.x, height: point.y))
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我假设不是需要tapAction,而是需要TapGesture或其他东西?但即使在那儿,我也看不到任何方法来获取有关水龙头位置的信息。我将如何处理?

小智 8

我能够通过做到这一点DragGesture(minimumDistance: 0)。然后使用startLocationValueonEnded找到水龙头的第一个位置。

  • 这种方法的问题是在可滚动的超级视图中。如果从视图开始滚动将是不可能的,因为它将被检测为“点击”。 (9认同)
  • @Rivera,您可以通过将此代码添加到可滚动超级视图来解决该问题: `.highPriorityGesture(DragGesture(minimumDistance: 10))` (2认同)

Log*_*gan 8

使用上面的一些答案,我制作了一个可能有用的 ViewModifier:

struct OnTap: ViewModifier {
    let response: (CGPoint) -> Void
    
    @State private var location: CGPoint = .zero
    func body(content: Content) -> some View {
        content
            .onTapGesture {
                response(location)
            }
            .simultaneousGesture(
                DragGesture(minimumDistance: 0)
                    .onEnded { location = $0.location }
            )
    }
}

extension View {
    func onTapGesture(_ handler: @escaping (CGPoint) -> Void) -> some View {
        self.modifier(OnTap(response: handler))
    }
}
Run Code Online (Sandbox Code Playgroud)

然后像这样使用:

Rectangle()
    .fill(.green)
    .frame(width: 200, height: 200)
    .onTapGesture { location in 
        print("tapped: \(location)")
    }
Run Code Online (Sandbox Code Playgroud)


小智 6

以防万一有人需要它,我已将上述答案转换为视图修饰符,该修饰符也将 CoordinateSpace 作为可选参数

import SwiftUI
import UIKit

public extension View {
  func onTapWithLocation(coordinateSpace: CoordinateSpace = .local, _ tapHandler: @escaping (CGPoint) -> Void) -> some View {
    modifier(TapLocationViewModifier(tapHandler: tapHandler, coordinateSpace: coordinateSpace))
  }
}

fileprivate struct TapLocationViewModifier: ViewModifier {
  let tapHandler: (CGPoint) -> Void
  let coordinateSpace: CoordinateSpace

  func body(content: Content) -> some View {
    content.overlay(
      TapLocationBackground(tapHandler: tapHandler, coordinateSpace: coordinateSpace)
    )
  }
}

fileprivate struct TapLocationBackground: UIViewRepresentable {
  var tapHandler: (CGPoint) -> Void
  let coordinateSpace: CoordinateSpace

  func makeUIView(context: UIViewRepresentableContext<TapLocationBackground>) -> UIView {
    let v = UIView(frame: .zero)
    let gesture = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.tapped))
    v.addGestureRecognizer(gesture)
    return v
  }

  class Coordinator: NSObject {
    var tapHandler: (CGPoint) -> Void
    let coordinateSpace: CoordinateSpace

    init(handler: @escaping ((CGPoint) -> Void), coordinateSpace: CoordinateSpace) {
      self.tapHandler = handler
      self.coordinateSpace = coordinateSpace
    }

    @objc func tapped(gesture: UITapGestureRecognizer) {
      let point = coordinateSpace == .local
        ? gesture.location(in: gesture.view)
        : gesture.location(in: nil)
      tapHandler(point)
    }
  }

  func makeCoordinator() -> TapLocationBackground.Coordinator {
    Coordinator(handler: tapHandler, coordinateSpace: coordinateSpace)
  }

  func updateUIView(_: UIView, context _: UIViewRepresentableContext<TapLocationBackground>) {
    /* nothing */
  }
}
Run Code Online (Sandbox Code Playgroud)


Moo*_*ose 6

也可以使用手势。

如果发生拖动或在点击或点击时触发动作,还有一些工作可以取消点击。

struct ContentView: View {
    
    @State var tapLocation: CGPoint?
    
    @State var dragLocation: CGPoint?

    var locString : String {
        guard let loc = tapLocation else { return "Tap" }
        return "\(Int(loc.x)), \(Int(loc.y))"
    }
    
    var body: some View {
        
        let tap = TapGesture().onEnded { tapLocation = dragLocation }
        let drag = DragGesture(minimumDistance: 0).onChanged { value in
            dragLocation = value.location
        }.sequenced(before: tap)
        
        Text(locString)
        .frame(width: 200, height: 200)
        .background(Color.gray)
        .gesture(drag)
    }
}
Run Code Online (Sandbox Code Playgroud)


Epa*_*aga 5

好吧,经过一番修改,并感谢对我的另一个问题的回答,我找到了一种使用UIViewRepresentable的方法(但是,一定要让我知道是否有更简单的方法!)为了我!

struct ContentView : View {
    @State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(x:50,y:50)]
    var body: some View {
        return ZStack(alignment: .topLeading) {
            Background {
                   // tappedCallback
                   location in
                    self.points.append(location)
                }
                .background(Color.white)
            ForEach(self.points.identified(by: \.debugDescription)) {
                point in
                Color.red
                    .frame(width:50, height:50, alignment: .center)
                    .offset(CGSize(width: point.x, height: point.y))
            }
        }
    }
}

struct Background:UIViewRepresentable {
    var tappedCallback: ((CGPoint) -> Void)

    func makeUIView(context: UIViewRepresentableContext<Background>) -> UIView {
        let v = UIView(frame: .zero)
        let gesture = UITapGestureRecognizer(target: context.coordinator,
                                             action: #selector(Coordinator.tapped))
        v.addGestureRecognizer(gesture)
        return v
    }

    class Coordinator: NSObject {
        var tappedCallback: ((CGPoint) -> Void)
        init(tappedCallback: @escaping ((CGPoint) -> Void)) {
            self.tappedCallback = tappedCallback
        }
        @objc func tapped(gesture:UITapGestureRecognizer) {
            let point = gesture.location(in: gesture.view)
            self.tappedCallback(point)
        }
    }

    func makeCoordinator() -> Background.Coordinator {
        return Coordinator(tappedCallback:self.tappedCallback)
    }

    func updateUIView(_ uiView: UIView,
                       context: UIViewRepresentableContext<Background>) {
    }

}
Run Code Online (Sandbox Code Playgroud)

  • @LouisLac我遇到了一个问题,当父 SwiftUI 视图之一具有“onTapGesture”(用于单击)时,带有“numberOfTapsRequired = 2”的“UITapGestureRecognizer”停止工作。解决这个问题的方法是将识别器包装在 SwiftUI 视图中,该视图具有无操作 `.onTapGesture(count: 2) {}` (2认同)

prz*_*iak 5

一个简单的解决方案是使用DragGesture并将minimumDistance参数设置为 0,使其类似于点击手势:

Color.gray
    .gesture(DragGesture(minimumDistance: 0).onEnded({ (value) in
        print(value.location) // Location of the tap, as a CGPoint.
    }))
Run Code Online (Sandbox Code Playgroud)

如果是点击手势,它将返回此点击的位置。但是,它还会返回拖动手势的结束位置——也称为“触摸事件”。可能不是想要的行为,所以请记住它。

  • 这可行,但不是在列表中使用的好解决方案 (2认同)

Lou*_*Lac 5

我想出的最正确且与 SwiftUI 兼容的实现是这个。您可以像使用任何常规 SwiftUI 手势一样使用它,甚至可以将其与其他手势结合使用,管理手势优先级等...

import SwiftUI

struct ClickGesture: Gesture {
    let count: Int
    let coordinateSpace: CoordinateSpace
    
    typealias Value = SimultaneousGesture<TapGesture, DragGesture>.Value
    
    init(count: Int = 1, coordinateSpace: CoordinateSpace = .local) {
        precondition(count > 0, "Count must be greater than or equal to 1.")
        self.count = count
        self.coordinateSpace = coordinateSpace
    }
    
    var body: SimultaneousGesture<TapGesture, DragGesture> {
        SimultaneousGesture(
            TapGesture(count: count),
            DragGesture(minimumDistance: 0, coordinateSpace: coordinateSpace)
        )
    }
    
    func onEnded(perform action: @escaping (CGPoint) -> Void) -> _EndedGesture<ClickGesture> {
        self.onEnded { (value: Value) -> Void in
            guard value.first != nil else { return }
            guard let location = value.second?.startLocation else { return }
            guard let endLocation = value.second?.location else { return }
            guard ((location.x-1)...(location.x+1)).contains(endLocation.x),
                  ((location.y-1)...(location.y+1)).contains(endLocation.y) else {
                return
            }  
            action(location)
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

上面的代码定义了一个符合 SwiftUIGesture协议的结构体。这个手势是 aTapGesture和 a 的组合DragGesture。这是确保手势是点击并同时检索点击位置所必需的。

onEnded方法检查两个手势是否都发生,并通过作为参数传递的转义闭包将位置作为 CGPoint 返回。最后两个guard语句在这里处理多个点击手势,因为用户可以点击稍微不同的位置,这些线引入了 1 点的容差,如果想要更大的灵活性,可以更改。

extension View {
    func onClickGesture(
        count: Int,
        coordinateSpace: CoordinateSpace = .local,
        perform action: @escaping (CGPoint) -> Void
    ) -> some View {
        gesture(ClickGesture(count: count, coordinateSpace: coordinateSpace)
            .onEnded(perform: action)
        )
    }
    
    func onClickGesture(
        count: Int,
        perform action: @escaping (CGPoint) -> Void
    ) -> some View {
        onClickGesture(count: count, coordinateSpace: .local, perform: action)
    }
    
    func onClickGesture(
        perform action: @escaping (CGPoint) -> Void
    ) -> some View {
        onClickGesture(count: 1, coordinateSpace: .local, perform: action)
    }
}
Run Code Online (Sandbox Code Playgroud)

最后,View扩展被定义为提供onDragGesture与其他原生手势相同的 API 。

像使用任何 SwiftUI 手势一样使用它:

struct ContentView : View {
    @State var points:[CGPoint] = [CGPoint(x:0,y:0), CGPoint(x:50,y:50)]
    var body: some View {
        return ZStack {
            Color.gray
                .onClickGesture { point in
                    points.append(point)
                }
            ForEach(self.points.identified(by: \.debugDescription)) {
                point in
                Color.red
                    .frame(width:50, height:50, alignment: .center)
                    .offset(CGSize(width: point.x, height: point.y))
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)