簡體   English   中英

如何檢測 SwiftUI 中的點擊手勢位置?

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

(對於 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))
            }
        }
    }
}

我假設不是 tapAction,我需要有一個 TapGesture 什么的? 但即使在那里,我也看不到任何獲取有關水龍頭位置信息的方法。 我怎么會go這個呢?

好吧,經過一番修修補補,感謝我對另一個問題的回答,我想出了一種使用 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>) {
    }

}

我可以用DragGesture(minimumDistance: 0)做到這一點。 然后使用startLocation上的Value中的onEnded找到水龍頭的第一個位置。

一個簡單的解決方案是使用DragGesture並將minimumDistance參數設置為 0,使其類似於點擊手勢:

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

在點擊手勢的情況下,它將返回此點擊的位置。 但是,它也會返回拖動手勢的結束位置——也稱為“觸摸事件”。 可能不是理想的行為,所以請記住這一點。

更新 iOS 16

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

原始答案

我想出的最正確和 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))
            }
        }
    }
}

也可以使用手勢。

如果發生拖動或在點擊向下或向上點擊時觸發動作,還有一些工作可以取消點擊。

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

以防萬一有人需要,我已將上述答案轉換為視圖修飾符,該修飾符也將 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 */
  }
}

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

DragGestureminimumDistance一起使用會破壞堆疊在其下的所有視圖上的滾動手勢。 使用simultaneousGesture手勢沒有幫助。 最終為我做的是使用將DragGesture排序到simultaneousGesture Gesture 內的TapGesture ,如下所示:

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

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

正如之前多次指出的那樣,當視圖已經在使用 DragGesture 時會出現問題,但通常在使用修飾符時會修復它:.simultaneousGesture(combinedClickGesture) 而不是 .gesture(combinedClickGesture)

將此發布給仍然需要支持的其他人 iOS 15。

也可以使用GeometryReaderCoordinateSpace 唯一的缺點是根據您的用例,您可能必須指定幾何閱讀器的大小。

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