【问题标题】:How to detect a tap gesture location in SwiftUI?如何在 SwiftUI 中检测点击手势位置?
【发布时间】:2019-10-24 02:51:52
【问题描述】:

(对于 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))
            }
        }
    }
}

我假设我需要 TapGesture 或其他东西,而不是 tapAction?但即使在那里,我也看不到任何获取水龙头位置信息的方法。我该怎么办?

【问题讨论】:

    标签: swift swiftui


    【解决方案1】:

    好吧,经过一番修补并感谢this answer 解决我的另一个问题,我找到了一种使用 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>) {
        }
    
    }
    

    【讨论】:

    • 最好的方法,但我会用.background 或更好的.overlay 修饰符添加Background,而不是使用额外的ZStack
    • 这很好用而且很干净。我还把它扔进了.background,而不是把它堆在ZStack下面。谢谢!
    • 当同一视图中还有 SwiftUI 手势时,此方法有效,但不能很好地处理手势优先级。
    • @LouisLac 我的UITapGestureRecognizernumberOfTapsRequired = 2 在父SwiftUI 视图之一具有onTapGesture(单击一次)时停止工作。修复它的原因是在 SwiftUI 视图中使用识别器包装视图,该视图具有无操作 .onTapGesture(count: 2) {}
    【解决方案2】:

    我可以通过DragGesture(minimumDistance: 0) 做到这一点。然后使用onEnded 上的Value 中的startLocation 找到水龙头的第一个位置。

    【讨论】:

    • 这种方法的问题是在可滚动的超级视图中。如果从视图开始滚动将是不可能的,因为它将被检测为“点击”。
    【解决方案3】:

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

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

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

    【讨论】:

    • 这可行,但不是在列表中使用的好解决方案
    【解决方案4】:

    也可以使用手势。

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

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

    【讨论】:

    • 谢谢!我尝试了很多手势组合!
    【解决方案5】:

    以防万一有人需要,我已将上述答案转换为视图修饰符,该修饰符也将 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 */
      }
    }
    

    【讨论】:

    • 这对我来说非常有效。我需要一个全局值,所以我只是将 CoordinateSpace = .local 更改为 CoordinateSpace = .global
    【解决方案6】:

    我想出的最正确且与 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)
            }
        }
    }
    

    以上代码定义了一个符合 SwiftUI Gesture 协议的结构体。这个手势是TapGestureDragGesture 的组合。这是确保手势是点击并同时检索点击位置所必需的。

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

    最后,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))
                }
            }
        }
    }
    

    【讨论】:

    • 虽然看起来很流畅,类似于简单的点击手势,但我建议谨慎使用它,因为它仍然是一个同时手势组合,可能会导致不良副作用.例如,可以在不破坏滚动的情况下将点击手势(和长按序列之前的点击)放在列表​​行上,而在上面的合成中拖动则完全破坏它。 ||对于简单的视图层次结构,该解决方案应该工作得很好。
    • 是的,不幸的是它仍然有缺点,但至少比UIViewRepresentable解决方案要少。
    【解决方案7】:

    使用上面的一些答案,我制作了一个可能有用的 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))
        }
    }
    

    然后像这样使用:

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-12-21
      • 1970-01-01
      • 2012-05-14
      • 2015-08-25
      • 1970-01-01
      • 2019-01-09
      相关资源
      最近更新 更多