Swiftui - How to Get Coordinate/Position of Clicked Button

SwiftUI - how to get coordinate/position of clicked Button

Turns out I solved this problem by adapting the example on https://swiftui-lab.com/communicating-with-the-view-tree-part-2/

The essence of the technique is using anchorPreference which is a means of sending data about one view back up the chain to ancestral views. I couldn't find any docs on this in the Apple world but I can attest that it works.

I'm not adding code here as the reference link also includes explanation that I don't feel qualified to re-iterate here!

SwiftUI - how to get the exactly center coordinate/position of newly created Button

One way to accomplish this is utilising "anchor preferences".

The idea is, to create the bounds anchor of the button when it is created and store it into an anchor preference.

To get an actual bounds value, we need a GeometryProxy where we relate the bounds anchor and get the bounds value.

When we have the bounds value, we store it in a state variable where they are accessible when the button action executes.

The following solution creates a number of buttons where the bounds are accessible via a Dictionary where the key is the button's label.

import SwiftUI

struct ContentView: View {

let labels = (0...4).map { "- \($0) -" }

@State private var bounds: [String: CGRect] = [:]

var body: some View {
VStack {
ForEach(labels, id: \.self) { label in
Button(action: {
let bounds = bounds[label]
print(bounds ?? "")
}) {
Text(verbatim: label)
}
// Store bounds anchors into BoundsAnchorsPreferenceKey:
.anchorPreference(
key: BoundsAnchorsPreferenceKey.self,
value: .bounds,
transform: { [label: $0] })
}
}
.frame(width: 300, height: 300, alignment: .center)
.backgroundPreferenceValue(BoundsAnchorsPreferenceKey.self) { anchors in
// Get bounds relative to VStack:
GeometryReader { proxy in
let localBoundss = anchors.mapValues { anchor in
CGRect(origin: proxy[anchor].origin, size: proxy[anchor].size)
}
Color.clear.border(Color.blue, width: 1)
.preference(key: BoundsPreferenceKey.self, value: localBoundss)
}
}
.onPreferenceChange(BoundsPreferenceKey.self) { bounds in
// Store bounds into the state variable:
self.bounds = bounds
}
}

}

extension CGRect: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(origin.x)
hasher.combine(origin.y)
hasher.combine(size.width)
hasher.combine(size.height)
}
}

struct BoundsAnchorsPreferenceKey: PreferenceKey {
typealias Value = [String: Anchor<CGRect>]
static var defaultValue: Value = [:]

static func reduce(value: inout Value, nextValue: () -> Value) {
value = value.merging(nextValue()) { (_, new) in new }
}
}

struct BoundsPreferenceKey: PreferenceKey {
typealias Value = [String: CGRect]
static var defaultValue: Value = [:]

static func reduce(value: inout Value, nextValue: () -> Value) {
value = value.merging(nextValue()) { (_, new) in new }
}
}

import PlaygroundSupport
PlaygroundPage.current.setLiveView(
NavigationView {
ContentView()
}
.navigationViewStyle(.stack)
)

The solution as is, looks a bit elaborated - but doesn't use any "tricks". We may alleviate this somewhat with using ViewModifiers.

Selecting nearest button according to finger position in SwiftUI

Here is possible approach. Tested with Xcode 11.4 / iOS 13.4

demo

struct SelectTheKey: View {

private var sArray = ["e", "s", "p", "b", "k"]
@State var isShowPopup: Bool = false
@State private var dragPosition = CGPoint.zero

@State private var rects = [Int: CGRect]()
@State private var selected = -1
var body: some View {

VStack() {
Spacer()
Text("global: \(self.dragPosition.x) : \(self.dragPosition.y)")

if isShowPopup {
HStack(spacing: 5) {
ForEach(0..<sArray.count) { id in
Text("\(self.sArray[id])").fontWeight(.bold).font(.title)
.foregroundColor(.white)
.padding()
.background(id == self.selected ? Color.red : Color.blue)
.cornerRadius(5)
.background(self.rectReader(for: id))
}
}.offset(x:40, y:0)
}

Text("A").frame(width: 60, height: 90)
.foregroundColor(.white)
.background(Color.purple)
.shadow(radius: 2)
.padding(10)
.gesture(DragGesture(minimumDistance: 2, coordinateSpace: .global)
.onChanged { dragGesture in
self.dragPosition = dragGesture.location
if let (id, _) = self.rects.first(where: { (_, value) -> Bool in
value.minX < dragGesture.location.x && value.maxX > dragGesture.location.x
}) { self.selected = id }

if !self.isShowPopup {self.isShowPopup.toggle()}
}
.onEnded {finalValue in
if self.isShowPopup {self.isShowPopup.toggle()}
})
}
}

func rectReader(for key: Int) -> some View {
return GeometryReader { gp -> AnyView in
let rect = gp.frame(in: .global)
DispatchQueue.main.async {
self.rects[key] = rect
}
return AnyView(Rectangle().fill(Color.clear))
}
}

}

SwiftUI Get Coordinates of TextField or any View

Here is a demo of how specific view coordinates can be read (see helpful comments inline)

struct DemoReadViewOrigin: View {

@State private var someNumber1 = "1000"
@State private var someNumber2 = "2000"
//bunch more

@State private var enteredNumber = "Some Text at the Top"
@State var value: CGFloat = 0

var body: some View {
ScrollView {
VStack {
Spacer()
Text("\(enteredNumber)")
Spacer()

Group { //1
TextField("Placeholder", text: $someNumber1)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
.background(GeometryReader { gp -> Color in
let rect = gp.frame(in: .named("OuterV")) // < in specific container
// let rect = gp.frame(in: .global) // < in window
// let rect = gp.frame(in: .local) // < own bounds
print("Origin: \(rect.origin)")
return Color.clear
})

//this does not work
.onTapGesture {
}

TextField("Placeholder", text: $someNumber2)
.keyboardType(.default)
.textFieldStyle(RoundedBorderTextFieldStyle())
}//group 1

//bunch more

Button(action: {
self.enteredNumber = self.someNumber1
self.someNumber1 = ""
// UIApplication.shared.endEditing()
}) {
Text("Submit")
}
.padding(.bottom, 50)

}//outer v
.coordinateSpace(name: "OuterV") // << declare coord space
.padding(.horizontal, 16)
.padding(.top, 44)

}//Scrollview or Form
// .modifier(AdaptsToSoftwareKeyboard())
}
}

Get Coordinates of Moving Pointer in SwiftUI (iPadOS)

The WWDC video covers this very topic:

Handle trackpad and mouse input

Add SupportsIndirectInputEvents to your Info.plist

From the video:

It is required in order to get the new touch type indirect pointer and
EventType.transform.
Existing projects do not have this key set and will need to add it. Starting with iOS 14 and macOS Big Sur SDKs, new UIKit and SwiftUI
projects will have this value set to "true."

In addition you will use UIPointerInteraction. This tutorial shows you step by step including custom cursors:

https://pspdfkit.com/blog/2020/supporting-pointer-interactions/

can I get the position of a `View` after layout in SwiftUI?

Use a GeometryReader to get the frame of each view and use the frame to determine the points for a path between the two views.

struct ContentView : View {
var body: some View {
GeometryReader { geometry -> Text in
let frame = geometry.frame(in: CoordinateSpace.local)
return Text("\(frame.origin.x), \(frame.origin.y), \(frame.size.width), \(frame.size.height)")
}
}
}

How to position my button to the Bottom of the Screen? SwiftUI

struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "heart.fill")
.resizable()
.frame(width: 60, height: 60)
.foregroundColor(.red)

Group{
Text("Welcome!")
.font(.title)

Button(action: {
print("tapped!")
}, label: {
Text("Continue")
.foregroundColor(.white)
.frame(width: 200, height: 40)
.background(Color.green)
.cornerRadius(15)
.padding()
})
}.frame(maxHeight: .infinity, alignment: .bottom)
}
}
}

Result:

Sample Image

can I get the tap position over a Text View?

Here is an approach how to get tap location in Text view coordinate space:

Text("hello world")
.gesture(DragGesture(minimumDistance: 0).onEnded { value in
print("Position: \(value.location)") // in local coordinates
})

How to get mouse location with SwiftUI?

With help from SwiftUILab@twitter

struct ContentView: View {

@State private var pt: CGPoint = .zero

var body: some View {

let myGesture = DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded({
self.pt = $0.startLocation
})

// Spacers needed to make the VStack occupy the whole screen
return VStack {

Spacer()

HStack {
Spacer()
Text("Tapped at: \(pt.x), \(pt.y)")
Spacer()
}

Spacer()

}
.border(Color.green)
.contentShape(Rectangle()) // Make the entire VStack tappabable, otherwise, only the areay with text generates a gesture
.gesture(myGesture) // Add the gesture to the Vstack
}
}


Related Topics



Leave a reply



Submit