Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - how to get coordinate/position of clicked Button

Tags:

swift

swiftui

Short version: How do I get the coordinates of a clicked Button in SwiftUI?

I'm looking for something like this (pseudo code) where geometry.x is the position of the clicked button in the current view:

GeometryReader { geometry in      
     return Button(action: { self.xPos = geometry.x}) {  
          HStack {               
               Text("Sausages")  
          }  
     }  
}

Long version: I'm beginning SwiftUI and Swift so wondering how best to achieve this conceptually.

To give the concrete example I am playing with:

Imagine a tab system where I want to move an underline indicator to the position of a clicked button.

[aside] There is a answer in this post that visually does what I am going for but it seems rather complicated: How to make view the size of another view in SwiftUI [/aside]

Here is my outer struct which builds the tab bar and the rectangle (the current indicator) I am trying to size and position:


import SwiftUI
import UIKit

struct TabBar: View {

    var tabs:ITabGroup
    @State private var selected = "Popular"
    @State private var indicatorX: CGFloat = 0
    @State private var indicatorWidth: CGFloat = 10
    @State private var selectedIndex: Int = 0

    var body: some View {
        ScrollView(.horizontal) {
            HStack(spacing: 0) {
                ForEach(tabs.tabs) { tab in
                    EachTab(tab: tab, choice: self.$selected, tabs: self.tabs, x: self.$indicatorX, wid: self.$indicatorWidth)
                }
            }.frame(minWidth: 0, maxWidth: .infinity, minHeight: 40, maxHeight: 40).padding(.leading, 10)
                .background(Color(UIColor(hex: "#333333")!))
            Rectangle()
                .frame(width: indicatorWidth, height: 3 )
                .foregroundColor(Color(UIColor(hex: "#1fcf9a")!))
            .animation(Animation.spring())
        }.frame(height: 43, alignment: .leading)
    }
}

Here is my struct that creates each tab item and includes a nested func to get the width of the clicked item:

struct EachTab: View {
    // INCOMING!
    var tab: ITab
    @Binding var choice: String
    var tabs: ITabGroup
    @Binding var x: CGFloat
    @Binding var wid: CGFloat
    @State private var labelWidth: CGRect = CGRect()

    private func tabWidth(labelText: String, size: CGFloat) -> CGFloat {
        let label = UILabel()
        label.text = labelText
        label.font = label.font.withSize(size)
        let labelWidth = label.intrinsicContentSize.width
        return labelWidth
    }

    var body: some View {
        Button(action: { self.choice = self.tab.title; self.x = HERE; self.wid = self.tabWidth(labelText: self.choice, size: 13)}) {
            HStack {
                // Determine Tab colour based on whether selected or default within black or green rab set
                if self.choice == self.tab.title {
                    Text(self.tab.title).foregroundColor(Color(UIColor(hex: "#FFFFFF")!)).font(.system(size: 13)).padding(.trailing, 10).animation(nil)
                } else {
                    Text(self.tab.title).foregroundColor(Color(UIColor(hex: "#dddddd")!)).font(.system(size: 13)).padding(.trailing, 10).animation(nil)
                }
            }
        }
        // TODO: remove default transition fade on button click
    }
}

Creating a non SwiftUI UILabel to get the width of the Button seems a bit wonky. Is there a better way?

Is there a simple way to get the coordinates of the clicked SwiftUI Button?

like image 838
Ben Frain Avatar asked Oct 10 '19 14:10

Ben Frain


1 Answers

You can use a DragGesture recogniser with a minimum drag distance of 0, which provides you the location info. However, if you combine the DragGesture with your button, the drag gesture won't be triggered on normal clicks of the button. It will only be triggered when the drag ends outside of the button.

You can get rid of the button completely, but of course then you lose the default button styling.

The view would look like this in that case:

struct MyView: View {

    @State var xPos: CGFloat = 0

    var body: some View {
        GeometryReader { geometry in
            HStack {
                Text("Sausages: \(self.xPos)")
            }
        }.gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded { dragGesture in
            self.xPos = dragGesture.location.x
        })
    }
}

The coordinateSpace parameter specifies if you want the touch position in .local or .global space. In the local space, the position is relative to the view that you've attached the gesture to. For example, if I had a Text view in the middle of the screen, my local y position would be almost 0, whereas my global y would be half of the screen height.

This tripped me up a bit, but this example shows the idea:

struct MyView2: View {

    @State var localY: CGFloat = 0
    @State var globalY: CGFloat = 0

    var body: some View {
        VStack {
            Text("local y: \(self.localY)")
                .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .local).onEnded { dragGesture in
                    self.localY = dragGesture.location.y
                })

            Text("global y: \(self.globalY)")
                .gesture(DragGesture(minimumDistance: 0, coordinateSpace: .global).onEnded { dragGesture in
                    self.globalY = dragGesture.location.y
                })
        }
    }
}
like image 64
omar Avatar answered Sep 23 '22 02:09

omar