How to make whole List row a NavigationLink in SwiftUI - swiftui

I am making a list of questions sets and want the whole row to navigate to next view (every row has a different destination view), but only the area next to the text is tappable. I tried creating a SetRow() outside of List and it was working completely fine, but when its inside of List only the empty area is working.
Red is the working area
List code here:
NavigationView {
VStack {
SearchBar(input: $searchInput)
List(viewModel.filterList(by: searchInput)) { setVM in
NavigationLink(destination:
SetView(viewModel: QuestionListViewModel(
emoji: setVM.questionSet.emoji,
title: setVM.questionSet.title,
setID: setVM.questionSet.id)
))
{
SetRow(viewModel: setVM)
}
.onLongPressGesture(minimumDuration: 1) {
selectedSetVM = setVM
showDeleteAlert.toggle()
}
}
.listStyle(PlainListStyle())
}
and SetRow code here:
HStack {
Text(viewModel.questionSet.emoji)
.font(.system(size: 50))
VStack(alignment: .leading) {
Text(viewModel.questionSet.title)
.font(.system(size: 20, weight: .semibold, design: .default))
VStack(alignment: .leading) {
Text("Questions: \(viewModel.questionSet.size)")
.font(.system(size: 15, weight: .light, design: .default))
.foregroundColor(.gray)
Text("Updated: \(viewModel.questionSet.lastUpdated, formatter: taskDateFormat)")
.font(.system(size: 15, weight: .light, design: .default))
.foregroundColor(.gray)
}
}
Spacer()
}
.padding(.top, 10)

The problem comes with your 'longPressGesture' which wants to control the content of your row and listen for longPresses. To avoid this you can use 'onTapGesture' to control the activation of the NavigationLink and 'onLongPressGesture' to activate your actionSheet or Alert.
Here is a short code example that demonstrates the usage:
https://stackoverflow.com/a/67099257

Related

swiftUI list selection lead to textfield

As I have constructed a list of country codes and I am able to tap on the list items. Here i want to show the tapped or selected country code inside my textfield which means, when I click on a country code it should show inside the textfield. I am new to swiftUI, it would be great if someone helped me to get this.
enter image description here
state variables given as:
#State private var text = ""
#State private var selection: String!
My textfield code goes here:
HStack {
TextField("Country code", text: $text).padding(.leading)
.frame(width: 385, height: 50)
.border(.gray)
.padding(.leading, 25)
.overlay(
Button(action: {
withAnimation {
showCodes.toggle()
}
}, label: {
Image(systemName: "chevron.down")
.foregroundColor(.gray)
.padding(.leading, 320)
.position(x: 215, y: 25)
})
)
.position(x: 195, y: 40)
.padding(.top, -570)
}
List code goes here :
if showCodes == true {
List (selection: $selection) {
ForEach(datas.users, id: \.dial_code) { user in
HStack{
Text(user.name)
.font(.callout)
.foregroundColor(Color.black)
Spacer()
Text(user.dial_code)
.font(.callout)
.foregroundColor(Color.blue)
}
}
}
.listRowInsets(EdgeInsets())
.frame(maxWidth: 400, maxHeight: .infinity, alignment: .leading)
.padding(.top, -585)
}
You can have it like:
ForEach(datas.users, id: \.dial_code) { user in
HStack {
Text(user.name)
.font(.callout)
.foregroundColor(Color.black)
Spacer()
Text(user.dial_code)
.font(.callout)
.foregroundColor(Color.blue)
}
.onTapGesture { text = user.dial_code }
}
I also suggest extracting the HStack along with it's content in a separate func like:
private func getCountryCodeCell(for user: User) -> some View {
HStack {
Text(user.name)
.font(.callout)
.foregroundColor(Color.black)
Spacer()
Text(user.dial_code)
.font(.callout)
.foregroundColor(Color.blue)
}
}

SwiftUI, remove space between views in a VStack?

Why is there so much space between the three blue rectangles and the list? How can I remove the space so that all views within the VStack stack at the top? I tried using a Spacer() directly after the List, but nothing changed.
struct ContentView: View {
init() { UITableView.appearance().backgroundColor = UIColor.clear }
var body: some View {
NavigationView {
ZStack {
Color.red
.ignoresSafeArea()
VStack {
HStack {
Text("Faux Title")
.font(.system(.largeTitle, design: .rounded))
.fontWeight(.heavy)
Spacer()
Button(action: {
// settings
}, label: {
Image(systemName: "gearshape.fill")
.font(.system(.title2))
})
}
.padding()
GeometryReader { geometry in
HStack() {
Text("1")
.frame(width: geometry.size.width * 0.30, height: 150)
.background(Color.blue)
Spacer()
Text("2")
.frame(width: geometry.size.width * 0.30, height: 150)
.background(Color.blue)
Spacer()
Text("3")
.frame(width: geometry.size.width * 0.30, height: 150)
.background(Color.blue)
}
}
.padding()
List {
Text("One")
Text("Two")
Text("Three")
Text("Four")
Text("Five")
Text("Six")
}
.listStyle(InsetGroupedListStyle())
}
}
.navigationBarHidden(true)
}
}
}
Bonus question: In web development, you can open your browser's Web Inspector and use the element selector to click on elements which highlights their borders. Useful for something like this where you're trying to figure out which element the offending spacing belongs to. Is there something like that in Xcode?
VStack(spacing: 0) {...}
Spacer()
to your question you can in Xcode use the view inspector. https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/ExaminingtheViewHierarchy.html
Since you know that your HStack with the blue rectangles is going to be a height of 150, you should constrain it to that using .frame(height: 150):
GeometryReader { geometry in
...
}
.padding()
.frame(height: 150) //Here
Otherwise, the GeometryReader will occupy all available vertical space.
Re: your web dev comparison, check out the Xcode view hierarchy inspector. It's not exactly the same, but it's in the same vein: https://developer.apple.com/library/archive/documentation/ToolsLanguages/Conceptual/Xcode_Overview/ExaminingtheViewHierarchy.html

How to navigate to different screens/views by tapping button which is in ForEach

I have created a dashboard with 9 buttons laid out on a lazyVGrid and a ForEach loop to display the various buttons.
(Click to expand)
Now, I want to navigate to various new screens based on the button pressed. Someone, please help me achieve this.
LazyVGrid(columns: row, spacing: 25) {
ForEach(Dashboard_Data) { data in
Button(action: {
// action
}) {
ZStack(alignment: Alignment(horizontal: .trailing, vertical: .top)) {
VStack(alignment: .leading, spacing: 5) {
Spacer(minLength: 0)
Text(data.data)
.font(.title)
.fontWeight(.bold)
.foregroundColor(.white)
HStack{
Spacer(minLength: 0)
Text(data.suggest)
.font(.system(size: 17, weight: .regular))
.lineLimit(2)
.foregroundColor(.white)
}
}.padding()
.background(Color(data.image))
.cornerRadius(20)
.shadow(color: Color.black.opacity(0.5), radius: 5, x: 0, y: 5)
Image(data.imageIcon)
.renderingMode(.template)
.foregroundColor(.white)
.font(.system(size: 25, weight: .regular))
.padding(10)
.background(Color.white.opacity(0.15))
.clipShape(Circle())
}
}
}
}
var body: some View{
LazyVGrid(columns: row, spacing: 25) {
ForEach(Dashboard_Data) { data in
NavigationLink(destination: getTheDestination(data)) {
//label/design of your button
}
}
}
}
private func getTheDestination(data:<put ur dataType of Dashboard_Data> ) -> some View {
// add some input parameter in this func to get data/informattion from your Dashboard_Data
// according to ur data return the destination view
if data.data == "your value" {
return Text( _ data.data)
}
return Text("Destination")
}

Top aligned label in WidgetKit extension

I am trying to create a WidgetKit extension for one of my apps.
I would like to have a top-aligned label and some dynamic content.
The problem is that all content is centred.
I want the label to be at the top and the container should fill the rest of the space. Within that container are the items (also not centred, starting from the top).
This is my code:
struct WidgetView : View
{
var data: DataProvider.Entry
var body: some View
{
Text("Test")
.font(Font.system(size: 14.0, weight: .medium, design: .default))
.fontWeight(.semibold)
.background(Color.white)
.foregroundColor(Color.red)
.padding(.top, 8.0)
ForEach(0 ..< 2, id: \.self)
{
_ in
Text("This is just some text")
}
}
}
This will result in the following view:
I tried adding a ScrollView and a LazyVStack. Now it looks just like I want it to look, but there is a red circle (probably not allowed in WidgetKit?):
Is this possible? This is my first contact with SwiftUI.
Use VStack with spacer at the bottom
VStack {
Text("Test")
.font(Font.system(size: 14.0, weight: .medium, design: .default))
.fontWeight(.semibold)
.background(Color.white)
.foregroundColor(Color.red)
.padding(.top, 8.0)
ForEach(0 ..< 2, id: \.self)
{
_ in
Text("This is just some text")
}
Spacer()
}

SwiftUI Button tap only on text portion

The background area of my button is not detecting user interaction. Only way to interact with said button is to tap on the Text/ Label area of the button. How to make entire Button tappable?
struct ScheduleEditorButtonSwiftUIView: View {
#Binding var buttonTagForAction : ScheduleButtonType
#Binding var buttonTitle : String
#Binding var buttonBackgroundColor : Color
let buttonCornerRadius = CGFloat(12)
var body: some View {
Button(buttonTitle) {
buttonActionForTag(self.buttonTagForAction)
}.frame(minWidth: (UIScreen.main.bounds.size.width / 2) - 25, maxWidth: .infinity, minHeight: 44)
.buttonStyle(DefaultButtonStyle())
.lineLimit(2)
.multilineTextAlignment(.center)
.font(Font.subheadline.weight(.bold))
.foregroundColor(Color.white)
.border(Color("AppHighlightedColour"), width: 2)
.background(buttonBackgroundColor).opacity(0.8)
.tag(self.buttonTagForAction)
.padding([.leading,.trailing], 5)
.cornerRadius(buttonCornerRadius)
}
}
The proper solution is to use the .contentShape() API.
Button(action: action) {
HStack {
Spacer()
Text("My button")
Spacer()
}
}
.contentShape(Rectangle())
You can change the provided shape to match the shape of your button; if your button is a RoundedRectangle, you can provide that instead.
I think this is a better solution, add the .frame values to the Text() and the button will cover the whole area 😉
Button(action: {
//code
}) {
Text("Click Me")
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 44, maxHeight: 44, alignment: .center)
.foregroundColor(Color.white)
.background(Color.accentColor)
.cornerRadius(7)
}
You can define content Shape for hit testing by adding modifier: contentShape(_:eoFill:)
And important thing is you have to apply inside the content of Button.
Button(action: {}) {
Text("Select file")
.frame(width: 300)
.padding(100.0)
.foregroundColor(Color.black)
.contentShape(Rectangle()) // Add this line
}
.background(Color.green)
.cornerRadius(4)
.buttonStyle(PlainButtonStyle())
Another
Button(action: {}) {
VStack {
Text("Select file")
.frame(width: 100)
Text("Select file")
.frame(width: 200)
}
.contentShape(Rectangle()) // Add this inside Button.
}
.background(Color.green)
.cornerRadius(4)
.buttonStyle(PlainButtonStyle())
This fixes the issue on my end:
var body: some View {
GeometryReader { geometry in
Button(action: {
// Action
}) {
Text("Button Title")
.frame(
minWidth: (geometry.size.width / 2) - 25,
maxWidth: .infinity, minHeight: 44
)
.font(Font.subheadline.weight(.bold))
.background(Color.yellow).opacity(0.8)
.foregroundColor(Color.white)
.cornerRadius(12)
}
.lineLimit(2)
.multilineTextAlignment(.center)
.padding([.leading,.trailing], 5)
}
}
Is there a reason why you are using UIScreen instead of GeometryReader?
Short Answer
Make sure the Text (or button content) spans the length of the touch area, AND use .contentShape(Rectangle()).
Button(action:{}) {
HStack {
Text("Hello")
Spacer()
}
.contentShape(Rectangle())
}
Long Answer
There are two parts:
The content (ex. Text) of the Button needs to be stretched
The content needs to be considered for hit testing
To stretch the content (ex. Text):
// Solution 1 for stretching content
HStack {
Text("Hello")
Spacer()
}
// Solution 2 for stretching content
Text("Hello")
.frame(maxWidth: .infinity, alignment: .leading)
// Alternatively, you could specify a specific frame for the button.
To consider content for hit testing use .contentShape(Rectangle()):
// Solution 1
Button(action:{}) {
HStack {
Text("Hello")
Spacer()
}
.contentShape(Rectangle())
}
// Solution 2
Button(action:{}) {
Text("Hello")
.frame(maxWidth: .infinity, alignment: .leading)
.contentShape(Rectangle())
}
You might be doing this:
Button { /*to do something on button click*/}
label: { Text("button text").foregroundColor(Color.white)}
.frame(width: 45, height: 45, alignment: .center)
.background(Color.black)
Solution:
Button(action: {/*to do something on button click*/ })
{
HStack {
Spacer()
Text("Buttton Text")
Spacer() } }
.frame(width: 45, height: 45, alignment: .center)
.foregroundColor(Color.white)
.background(Color.black).contentShape(Rectangle())
A bit late to the answer, but I found two ways to do this —
Option 1: Using Geometry Reader
Button(action: {
}) {
GeometryReader { geometryProxy in
Text("Button Title")
.font(Font.custom("SFProDisplay-Semibold", size: 19))
.foregroundColor(Color.white)
.frame(width: geometryProxy.size.width - 20 * 2) // horizontal margin
.padding([.top, .bottom], 10) // vertical padding
.background(Color.yellow)
.cornerRadius(6)
}
}
Option 2: Using HStack with Spacers
HStack {
Spacer(minLength: 20) // horizontal margin
Button(action: {
}) {
Text("Hello World")
.font(Font.custom("SFProDisplay-Semibold", size: 19))
.frame(maxWidth:.infinity)
.padding([.top, .bottom], 10) // vertical padding
.background(Color.yellow)
.foregroundColor(Color.white)
.cornerRadius(6)
}
Spacer(minLength: 20)
}.frame(maxWidth:.infinity)
My thought process here is that although option 1 is more succinct, I would choose option 2 since it's less coupled to its parent's size (through GeometryReader) and more in line of how I think SwiftUI is meant to use HStack, VStack, etc.
I was working with buttons and texts that need user interaction when I faced this same issue. After looking and testing many answers (including some from this post) I ended up making it works in the following way:
For buttons:
/* WITH IMAGE */
Button {
print("TAppeD")
} label: {
Image(systemName: "plus")
.frame(width: 40, height: 40)
}
/* WITH TEXT */
Button {
print("TAppeD")
} label: {
Text("My button")
.frame(height: 80)
}
For Texts:
Text("PP")
.frame(width: 40, height: 40)
.contentShape(Rectangle())
.onTapGesture {
print("TAppeD")
}
In the case of the texts, I only need the .contentShape(Rectangle()) modifier when the Text doesn't have a .background in order to make the entire Text frame responsive to tap gesture, while with buttons I use my Text or Image view with a frame and neither a .background nor a .contentShape is needed.
Image of the following code in preview (I'm not allowed to include pictures yet )
import SwiftUI
struct ContentView: View {
#State var tapped: Bool = true
var body: some View {
VStack {
RoundedRectangle(cornerRadius: 19)
.frame(width: 40, height: 40)
.foregroundColor(tapped ? .red : .green)
Spacer()
HStack (spacing: 0) {
Text("PP")
.frame(width: 40, height: 40)
.contentShape(Rectangle())
.onTapGesture {
tapped.toggle()
}
Button {
print("TAppeD")
tapped.toggle()
} label: {
Image(systemName: "plus")
.frame(width: 40, height: 40)
}
.background(Color.red)
Button {
print("TAppeD")
tapped.toggle()
} label: {
Text("My button")
.frame(height: 80)
}
.background(Color.yellow)
}
Spacer()
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
this way makes the button area expand properly
but if the color is .clear, it dosen't work🤷‍♂️
Button(action: {
doSomething()
}, label: {
ZStack {
Color(.white)
Text("some texts")
}
})
When I used HStack then it worked for button whole width that's fine, But I was facing issue with whole button height tap not working at corners and I fixed it in below code:
Button(action:{
print("Tapped Button")
}) {
VStack {
//Vertical whole area covered
Text("")
Spacer()
HStack {
//Horizontal whole area covered
Text("")
Spacer()
}
}
}
If your app needs to support both iOS/iPadOS and macOS, you may want to reference my code!
Xcode 14.1 / iOS 14.1 / macOS 13.0 / 12-09-2022
Button(action: {
print("Saved to CoreData")
}) {
Text("Submit")
.frame(minWidth: 100, maxWidth: .infinity, minHeight: 44, maxHeight: 60, alignment: .center)
.foregroundColor(Color.white)
#if !os(macOS)
.background(Color.accentColor)
#endif
}
#if os(macOS)
.background(Color.accentColor)
#endif
.cornerRadius(7)
Easier work around is to add .frame(maxWidth: .infinity) modifier.
and wrap your button inside a ContainerView. you can always change the size of the button where it's being used.
Button(action: tapped) {
HStack {
if let icon = icon {
icon
}
Text(title)
}
.frame(maxWidth: .infinity) // This one
}