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
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:
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
Dyld: Library Not Loaded: @Rpath/Libswiftdispatch.Dylib
Easiest Way to Increment a Data Point in Firebase
How to Implement a Billboard Effect (Lookat Camera) in Realitykit
Spritekit Skphysicsjointfixed Odd Behaviour
How to Add a Loading View for Apple Watch
Swift 3 Closure Overload Resolution
Swift Compiler Error Int Is Not Convertible to Cgfloat
Gcdasyncsocket Multiple Connections Wont Accept Data from Multiple Sockets
How to Use Multiple Mtlrenderpipelinestates in One Mtlrendercommandencoder
How to Get Walking and Running Distance Using Healthkit in Swift
How to Create Apple Watchos5 Complication
Compound Key in Realm with Lazy Property
Provide Simple Method to Get Current Speed (Implementing Speedometer)
Property with '= {Return}()' or '{Return}'
Delay a Repeating Animation in Swiftui with Between Full Autoreverse Repeat Cycles