I have a SwiftUI List on iOS14.3 (Xcode12.3) and inside it's cells I want to have a Menu popup up when the user is tapping on a button. Additionally these cells have an onTapGesture and the problem is that this gesture is also triggered when the menu button is pressed.
Example:
List {
HStack {
Text("Cell content")
Spacer()
// Triggers also onTapGesture of cell
Menu(content: {
Button(action: { }) {
Text("Menu Item 1")
Image(systemName: "command")
}
Button(action: { }) {
Text("Menu Item 2")
Image(systemName: "option")
}
Button(action: { }) {
Text("Menu Item 3")
Image(systemName: "shift")
}
}) {
Image(systemName: "ellipsis")
.imageScale(.large)
.padding()
}
}
.background(Color(.systemBackground))
.onTapGesture {
print("Cell tapped")
}
}
If you tap on the ellipsis image, the menu opens but also "Cell tapped" will be printed to the console. This is a problem for me, because in my real world example I am collapsing the cell within the tap gesture and of course I don't want that to happen when the user presses the menu button.
I found out, that when I long press on the button the gesture won't be triggered. But this is not acceptable UX in my opinion, took me a while to find that out for my self.
I also noticed, when I replace the Menu with a regular Button, then the cells gesture is not triggered while pressing (only with BorderlessButtonStyle or PlainButtonStyle, otherwise he is not active at all). But I don't know how to open a Menu from it's action.
List {
HStack {
Text("Cell content")
Spacer()
// Does not trigger cells onTapGesture, but how to open Menu from action?
Button(action: { print("Button tapped") }) {
Image(systemName: "ellipsis")
.imageScale(.large)
.padding()
}
.buttonStyle(BorderlessButtonStyle())
}
.background(Color(.systemBackground))
.onTapGesture {
print("Cell tapped")
}
}
Alternatively, you can override Menu onTapGesture:
struct ContentView: View {
var body: some View {
List {
HStack {
Text("Cell content")
Spacer()
Menu(content: {
Button(action: {}) {
Text("Menu Item 1")
Image(systemName: "command")
}
Button(action: {}) {
Text("Menu Item 2")
Image(systemName: "option")
}
Button(action: {}) {
Text("Menu Item 3")
Image(systemName: "shift")
}
}) {
Image(systemName: "ellipsis")
.imageScale(.large)
.padding()
}
.onTapGesture {} // override here
}
.contentShape(Rectangle())
.onTapGesture {
print("Cell tapped")
}
}
}
}
Also, there's no need to use .background(Color(.systemBackground)) to tap on spacers. This is not really flexible - what if you want change background color?
You can use contentShape instead:
.contentShape(Rectangle())
I'm not sure if this is a bug or expected behavior, but instead of fighting with it I would recommend to avoid such overlapping (because even with success today it might stop working on different systems/updates).
Here is possible solution (tested with Xcode 12.1 / iOS 14.1)
List {
HStack {
// row content area
HStack {
Text("Cell content")
Spacer()
}
.background(Color(.systemBackground)) // for tap on spacer area
.onTapGesture {
print("Cell tapped")
}
// menu area
Menu(content: {
Button(action: { }) {
Text("Menu Item 1")
Image(systemName: "command")
}
Button(action: { }) {
Text("Menu Item 2")
Image(systemName: "option")
}
Button(action: { }) {
Text("Menu Item 3")
Image(systemName: "shift")
}
}) {
Image(systemName: "ellipsis")
.imageScale(.large)
.padding()
}
}
}
Related
I am using a NavigationView to navigate between views. As I don't want to use the NavigationBar, I am hiding it. Unfortunately, the area where the NavigationBar was (it is hidden) is still blocking the UI. A button there cannot be tapped.
How can I fix this?
// First View
struct ContentView: View {
var body: some View {
NavigationView {
VStack {
Spacer()
Text("Title")
Spacer()
NavigationLink(destination: View2()) {
Text("Next")
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
Spacer()
}
}
}
}
and
// Second View: Here the button cannot be tapped
struct View2: View {
var body: some View {
NavigationView {
VStack {
Button(action: {
print("this button doesn't work")
}, label: {
Text("Do something")
})
Spacer()
}
.padding(.top, 50)
.edgesIgnoringSafeArea(.all)
}
.navigationBarTitle("", displayMode: .inline)
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
The issue is due to used second NavigationView - it is wrong, there should be only one root navigation view which manages navigation stack in this case.
So here is fixed view (child one)
struct View2: View {
var body: some View {
VStack {
Button(action: {
print("this button doesn't work")
}, label: {
Text("Do something")
})
Spacer()
}
.padding(.top, 50)
.edgesIgnoringSafeArea(.all)
.navigationBarBackButtonHidden(true)
.navigationBarHidden(true)
}
}
Tested with Xcode 13.2 / iOS 15.2
Note: as you hide navigation but you don't need mode modifier as well, so removed .navigationBarTitle("", displayMode: .inline). Just for your info.
I want a sidebar to be displayed on the iPad. However, what bothers me about the Swiftui Navigazion View is that I have this ugly toggle button. Furthermore I would like to show a sidebar when the iPad is held horizontally. Can I change the Navigation View component so that this works?
no, but you can custom build your own:
struct ContentView: View {
#State private var selection: Int? = nil
var body: some View {
HStack {
List {
Button { selection = 1
} label: {
Text("Item 1")
}
Button { selection = 2
} label: {
Text("Item 2")
}
Button { selection = 3
} label: {
Text("Item 3")
}
}
.frame(width: 200)
.frame(maxHeight: .infinity)
.background(.gray.opacity(0.3))
VStack {
if selection != nil {
// Detail View
Text("Your detail view \(selection!)")
.font(.title)
} else {
Text("Select an item")
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center)
}
}
}
However, what bothers me about the Swiftui Navigazion View is that I have this ugly toggle button
The possible workaround to avoid button is to hide navigation bar, then in landscape (aka horizontal) you will see just sidebar
NavigationView {
VStack {
Text("Header")
.padding()
List(0..<100, id: \.self) { i in
NavigationLink(
tag: i,
selection: $activeLink,
destination: { Text("Details for \(i)") }
) {
Text("Row #\(i)")
}
}
}
.navigationBarHidden(true) // << here !!
Text("Default Details")
}
I have tried to use Buttons and Navigation Links from various examples when researched on this channel and on the net. The NavigationLink would be ok, except that the NavigationView is pushing everything down in my view.
I have a view that contains an image and a text like this: ( x Close) but when I use the code below, the Close button is not doing anything.
In ContentView() I have a (?) button that takes me from WalkthroughView(), then to the PageTabView, then to this view, TabDetailsView:
ContentView():
ZStack {
NavigationView {
VStack {
Text("Hello World")
.padding()
.font(.title)
.background(Color.red)
.foregroundColor(.white)
}
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button {
withAnimation {
showOnBoarding = true
}
} label: {
Image(systemName: "questionmark.circle.fill")
}
}
}
}
.accentColor(.red)
.disabled(showOnBoarding)
.blur(radius: showOnBoarding ? 3.0 : 0)
if showOnBoarding {
WalkthroughView(isWalkthroughViewShowing: $isWalkthroughViewShowing)
}
}
.onAppear {
if !isWalkthroughViewShowing {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
withAnimation {
showOnBoarding.toggle()
isWalkthroughViewShowing = true
}
}
}
}
WalkthroughView():
var body: some View {
ZStack {
GradientView()
VStack {
PageTabView(selection: $selection)
// shows Previous/Next buttons only
ButtonsView(selection: $selection)
}
}
.transition(.move(edge: .bottom))
}
PageTabView():
var body: some View {
TabView(selection: $selection) {
ForEach(tabs.indices, id: \.self) { index in
TabDetailsView(index: index)
}
}
.tabViewStyle(PageTabViewStyle())
}
below, is the TabDetailsView():
At the top of the view is this Close button, when pressed, should send me back to ContentView, but nothing is happening.
struct TabDetailsView: View {
#Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
let index: Int
then, inside the body:
VStack(alignment: .leading) {
Spacer()
VStack(alignment: .leading) {
// Button to close each walkthrough page...
Button(action: {
self.presentationMode.wrappedValue.dismiss()
}) {
Image(systemName: "xmark.circle.fill")
Text("Close")
}
.padding(.leading)
.font(.title2)
.accentColor(.orange)
Spacer()
VStack {
Spacer()
Image(tabs[index].image)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 415)
.padding(.leading, 10)
Text(tabs[index].title)
.font(.title)
.bold()
Text(tabs[index].text)
.padding()
Spacer()
}
.foregroundColor(.white)
}
}
if showOnBoarding {
WalkthroughView(isWalkthroughViewShowing: $isWalkthroughViewShowing)
}
Inserting view like above is not a presentation in standard meaning, that's why provided code does not work.
As this view is shown via showOnBoarding it should be hidden also via showOnBoarding, thus the solution is to pass binding to this state into view where it will be toggled back.
Due to deep hierarchy the most appropriate way is to use custom environment value. For simplicity let's use ResetDefault from https://stackoverflow.com/a/61847419/12299030 (you can rename it in your code)
So required modifications:
if showOnBoarding {
WalkthroughView(isWalkthroughViewShowing: $isWalkthroughViewShowing)
.environment(\.resetDefault, $showOnBoarding)
}
and in child view
struct TabDetailsView: View {
#Environment(\.resetDefault) var showOnBoarding
// .. other code
Button(action: {
self.showOnBoarding.wrappedValue.toggle()
}) {
Image(systemName: "xmark.circle.fill")
Text("Close")
}
I have created a reusable component that has a picker, textfield and button within a form. However, with the button present, a tap on the picker field does not go to the picker. Rather it executes the button code. The TextField works fine. If I remove the button code, the proper behavior will occur with the picker. So the question is how to have both elements within this component? Please note that the preview adds the Navigation and Form which would otherwise come from the parent view.
var body: some View {
HStack {
if !showFee {
Spacer()
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
self.showFee.toggle()
}
}) {
Image(systemName: "plus.circle")
.font(.largeTitle)
}
Spacer()
} else {
VStack(spacing:20) {
Picker(selection: $feeSelection, label: Text("Fee Type")) {
ForEach(0 ..< fees.count) {
Text(self.fees[$0])
}
}
TextField("Fee Amount: $", value: $feeAmount, formatter: NumberFormatter.currency)
.keyboardType(.decimalPad)
Divider()
Button(action: {
withAnimation(.easeInOut(duration: 0.3)) {
self.showFee.toggle()
}
}) {
Image(systemName: "trash.circle.fill")
.font(.largeTitle)
.foregroundColor(.red)
}
}
}
}
.padding()
}
}
struct FeeCell_Previews: PreviewProvider {
static var previews: some View {
NavigationView {
Form {
FeeCell()
}
}
}
}
What you can do is to apply the PlainButtonStyle to your button. This will stop the button's tap covering to the whole cell in the Form:
Button(action: {}) {
Text("Button")
}
.buttonStyle(PlainButtonStyle())
I'm trying to implement a programmable NavigationLink to go to another view, triggered by a simple button. But when that button is next to another button, instead of triggering the NavigationLink, it triggers the action of the button next to it.
struct NavLinkView: View {
#State var showPasswordStr = true
#State var showCheckView = false
#State var password = "Abc"
var body: some View {
Form {
VStack {
NavigationLink(destination: CheckView(), isActive: $showCheckView ) {
EmptyView()
}
Text("Password")
.font(.system(size: 12, weight: .light, design: .default))
.foregroundColor(.gray)
HStack {
if showPasswordStr {
TextField("", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
} else {
SecureField("", text: $password)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
Button(action: {
showPasswordStr.toggle()
} ) {
Image(systemName: showPasswordStr ? "eye.slash" : "eye" )
}
.padding(.leading)
Button(action: { showCheckView.toggle() } ) {
Image(systemName: "checkmark.circle" )
}
.padding(.leading)
}
}
}
}
}
What am I mising? If I move the NavigationLink to be after the VStack, then both buttons trigger both the action of the first button and the NavigationLink.
As your buttons are in a Form you need to change their style to PlainButtonStyle:
Button(action: {
self.showPasswordStr.toggle()
}) {
Image(systemName: showPasswordStr ? "eye.slash" : "eye")
}
.buttonStyle(PlainButtonStyle()) // <- add style
Button(action: { self.showCheckView.toggle() }) {
Image(systemName: "checkmark.circle")
}
.buttonStyle(PlainButtonStyle()) // <- add style