简体   繁体   English

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

[英]How to detect a tap gesture location in SwiftUI?

(For SwiftUI, not vanilla UIKit) Very simple example code to, say, display red boxes on a gray background: (对于 SwiftUI,不是 vanilla 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))
            }
        }
    }
}

I'm assuming instead of tapAction, I need to have a TapGesture or something?我假设不是 tapAction,我需要有一个 TapGesture 什么的? But even there I don't see any way to get information on the location of the tap.但即使在那里,我也看不到任何获取有关水龙头位置信息的方法。 How would I go about this?我怎么会go这个呢?

Well, after some tinkering around and thanks to this answer to a different question of mine, I've figured out a way to do it using a UIViewRepresentable (but by all means, let me know if there's an easier way!) This code works for me!好吧,经过一番修修补补,感谢我对另一个问题的回答,我想出了一种使用 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>) {
    }

}

I was able to do this with a DragGesture(minimumDistance: 0) .我可以用DragGesture(minimumDistance: 0)做到这一点。 Then use the startLocation from the Value on onEnded to find the tap's first location.然后使用startLocation上的Value中的onEnded找到水龙头的第一个位置。

An easy solution is to use the DragGesture and set minimumDistance parameter to 0 so that it resembles the tap gesture:一个简单的解决方案是使用DragGesture并将minimumDistance参数设置为 0,使其类似于点击手势:

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

In case of a tap gesture it will return the location of this tap.在点击手势的情况下,它将返回此点击的位置。 However, it will also return the end location for a drag gesture – what's also referred to as a "touch up event".但是,它也会返回拖动手势的结束位置——也称为“触摸事件”。 Might not be the desired behavior, so keep it in mind.可能不是理想的行为,所以请记住这一点。

Update iOS 16更新 iOS 16

Starting form iOS 16 / macOS 13, the onTapGesture modifier makes available the location of the tap/click in the action closure:从 iOS 16 / macOS 13 开始, onTapGesture修饰符使动作闭包中的点击/单击位置可用:

struct ContentView: View {
  var body: some View {
    Rectangle()
      .frame(width: 200, height: 200)
      .onTapGesture { location in 
        print("Tapped at \(location)")
      }
  }
}

Original Answser原始答案

The most correct and SwiftUI-compatible implementation I come up with is this one.我想出的最正确和 SwiftUI 兼容的实现就是这个。 You can use it like any regular SwiftUI gesture and even combine it with other gestures, manage gesture priority, etc...您可以像使用任何常规 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)
        }
    }
}

The above code defines a struct conforming to SwiftUI Gesture protocol.上面的代码定义了一个符合 SwiftUI Gesture协议的结构体。 This gesture is a combinaison of a TapGesture and a DragGesture .这个手势是TapGestureDragGesture的组合。 This is required to ensure that the gesture was a tap and to retrieve the tap location at the same time.这是确保手势是点击并同时检索点击位置所必需的。

The onEnded method checks that both gestures occurred and returns the location as a CGPoint through the escaping closure passed as parameter. onEnded方法检查两个手势是否发生,并通过作为参数传递的转义闭包将位置作为 CGPoint 返回。 The two last guard statements are here to handle multiple tap gestures, as the user can tap slightly different locations, those lines introduce a tolerance of 1 point, this can be changed if ones want more flexibility.最后两个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)
    }
}

Finally View extensions are defined to offer the same API as onDragGesture and other native gestures.最后, View扩展被定义为提供与onDragGesture和其他原生手势相同的 API。

Use it like any SwiftUI gesture:像使用任何 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))
            }
        }
    }
}

It is also possible to use gestures.也可以使用手势。

There is a few more work to cancel the tap if a drag occurred or trigger action on tap down or tap up..如果发生拖动或在点击向下或向上点击时触发动作,还有一些工作可以取消点击。

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)
    }
}

Just in case someone needs it, I have converted the above answer into a view modifier which also takes a CoordinateSpace as an optional parameter以防万一有人需要,我已将上述答案转换为视图修饰符,该修饰符也将 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 */
  }
}

Using some of the answers above, I made a ViewModifier that is maybe useful:使用上面的一些答案,我制作了一个可能有用的 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))
    }
}

Then use like so:然后像这样使用:

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

Using DragGesture with minimumDistance broke scroll gestures on all the views that are stacked under.DragGestureminimumDistance一起使用会破坏堆叠在其下的所有视图上的滚动手势。 Using simultaneousGesture did not help.使用simultaneousGesture手势没有帮助。 What ultimately did it for me was using sequencing the DragGesture to a TapGesture inside simultaneousGesture , like so:最终为我做的是使用将DragGesture排序到simultaneousGesture Gesture 内的TapGesture ,如下所示:

.simultaneousGesture(TapGesture().onEnded {
    // Do something                                    
}.sequenced(before: DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded { value in
    print(value.startLocation)
}))

In iOS 16 and MacOS 13 there are better solutions, but to stay compatible with older os versions, I use this rather simple gesture, which also has the advantage of distinguish between single- and double-click.在 iOS 16 和 MacOS 13 中有更好的解决方案,但为了兼容旧版本的操作系统,我使用了这个相当简单的手势,它也有区分单击和双击的优点。

 var combinedClickGesture: some Gesture {
    SimultaneousGesture(ExclusiveGesture(TapGesture(count: 2),TapGesture(count: 1)), DragGesture(minimumDistance: 0) )
        .onEnded { value in
            if let v1 = value.first {
                var count: Int
                switch v1 {
                case .first():  count = 2
                case .second(): count = 1
                }
                if let v2 = value.second {
                    print("combinedClickGesture couunt = \(count) location = \(v2.location)")
                }
            }
        }
}

As pointed out several times before it´sa problem when the view already is using DragGesture, but often it is fixed when using the modifier: .simultaneousGesture(combinedClickGesture) instead of.gesture(combinedClickGesture)正如之前多次指出的那样,当视图已经在使用 DragGesture 时会出现问题,但通常在使用修饰符时会修复它:.simultaneousGesture(combinedClickGesture) 而不是 .gesture(combinedClickGesture)

Posting this for others who still have to support iOS 15.将此发布给仍然需要支持的其他人 iOS 15。

It's also possible using GeometryReader and CoordinateSpace .也可以使用GeometryReaderCoordinateSpace The only downside is depending on your use case you might have to specify the size of the geometry reader.唯一的缺点是根据您的用例,您可能必须指定几何阅读器的大小。

VStack {
    Spacer()

    GeometryReader { proxy in
        Button {
            print("Global tap location: \(proxy.frame(in: .global).center)")
            print("Custom coordinate space tap location: \(proxy.frame(in: .named("StackOverflow")))")
        } label: {
            Text("Tap me I know you want it")
        }
        .frame(width: 42, height: 42)
    }
    .frame(width: 42, height: 42)

    Spacer()
}
.coordinateSpace(name: "StackOverflow")

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM