Reputation: 1432
I have a Menu in my app, and I trigger haptic feedback when menu opens (from onTagGesture
action).
However sometimes when I tap on the menu open trigger, the menu won't actually open, but I'm still getting haptic feedback. I want haptic only when menu actually will open.
Here's the code simplified code chunk:
Menu {
Button("Menu button", action: someAction}}
} label: {
Text("Open menu") //in reality, this a more complicated view tapping on which should open the (context) menu
}
.onTagGesture {
let generator = UIImpactFeedbackGenerator(style: .rigid)
generator.impactOccurred()
}
So it is is pretty simple - tap on the Open menu
trigger, tap gets registered, haptic is played back, and menu opens.
But as mentioned, for whatever reason, sometimes I press on the Open menu
element, haptic plays back, but the actual menu won't open.
Whatever reasons for that are, I was wondering if there's any way at all to perform actions (such as the fore-mentioned haptic feedback), once menu has actually opened (or better yet, will actually open)? I tried to search wherever I could, and came up with nothing.
This is also important because menu opens on a long taps as well, that being iOS standard actions for opening menus. And even though I could add another separate handler for long taps (to provide haptics for both cases), this doesn't seems like a proper approach at all.
Combined with the fact that sometimes menu won't open, I definitely seem to need some other solution. Anyone can share any ideas? Is there some sort of onXXXXX handler I'm missing, that would fire when menu will open?
Thanks!
PS: To give more detail, I'm trying to implement this approach to menus described in Apple dev docs: https://developer.apple.com/documentation/swiftui/menu
As a part of the process I tried to attach onAppear handler to the whole menu, as well as to an individual element inside menu. Neither seems to be working.
Menu {
Button("Open in Preview", action: openInPreview)
Button("Save as PDF", action: saveAsPDF)
.onAppear { doHaptic() } //only fires once, when menu opens, but not for subsequent appearances
} label: {
Label("PDF", systemImage: "doc.fill")
}
.onAppear { doHaptic() } //doesn't really as it fires when the menu itself appears on the screen as a child of a parent view.
Upvotes: 18
Views: 7524
Reputation: 36
Just using the .onTapGesture
on the Menu itself works here. Tested on iOS 17
Menu {
Button("Open in Preview", action: {})
Button("Save as PDF", action: {})
} label: {
Label("PDF", systemImage: "doc.fill")
}
.onTapGesture { print("menu tapped") }
Upvotes: 1
Reputation: 215
Asperi's answer was not working for me on XCode 15.1 and iOS 17. However, primaryAction
works: https://developer.apple.com/documentation/swiftui/menu#Primary-action
Note: my use case was inside a .toolbar { ... }
Note 2: At least when used inside the navigation bar, Menu
's primaryAction
seems to interfere with a button's border-style (e.g. .buttonStyle(.borderless)
gets ignored) and nav bar item's hover effect.
Menu {
Button("Open in Preview", action: {})
Button("Save as PDF", action: {})
} label: {
Label("PDF", systemImage: "doc.fill")
} primaryAction: {
print("primary action")
}
Upvotes: 2
Reputation: 2770
I found that wrapping menu items in VStack and adding onAppear
/ onDisappear
to it, will enable you to track menu state.
Menu {
VStack {
ForEach(channels, id: \.id) { c in
Button {
self.channel = c
} label: {
Text(c.name)
}
}
}
.onAppear {
debugPrint("Shown")
}
.onDisappear {
debugPrint("Hidden")
}
} label: {
Image(systemName: "list.bullet")
}
Upvotes: -1
Reputation: 257711
The following variant seems works (tested with Xcode 13.3 / iOS 15.4)
Menu {
Button("Open in Preview", action: {})
Button("Save as PDF", action: {})
} label: {
Label("PDF", systemImage: "doc.fill")
}
.contentShape(Rectangle())
.simultaneousGesture(TapGesture().onEnded {
print(">> tapped")
})
*but, pay attention that gesture is resolved not only when tap-to-open, but for tap-to-close as well.
Upvotes: 6
Reputation: 1850
You could use onAppear
for that. Use it on the menu and it will only be called if the menu appears. E.g. below:
struct ContentView: View {
@State var menuOpen: Bool = false
// Just your button that triggers the menu
var body: some View {
Button(action: {
self.menuOpen.toggle()
}) {
if menuOpen {
MenuView(menuOpen: $menuOpen)
} else {
Image(systemName: "folder")
}
}
}
}
struct MenuView: View {
@Binding var menuOpen: Bool
// the menu view
var body: some View {
Rectangle()
.frame(width: 200, height: 200)
.foregroundColor(Color.red)
.overlay(Text("Menu Open").foregroundColor(Color.white))
.onAppear(perform: self.impactFeedback) // <- Use onAppear on the menu view to trigger it
}
// if function called it triggers the impact
private func impactFeedback() {
let generator = UIImpactFeedbackGenerator(style: .rigid)
generator.impactOccurred()
print("triggered impact")
}
}
Tested and working on Xcode 12.4
Upvotes: -2