19

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.
2
  • 1
    Did you ever figure this out? None of the mentioned solutions seem to work. I tried onTapGesture, primaryAction,simultaneousGesture Commented Jan 24 at 20:43
  • Using UIKit worked for me: stackoverflow.com/a/77918963/14351818 Commented Jan 25 at 18:59

7 Answers 7

6

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.

Sign up to request clarification or add additional context in comments.

5 Comments

No need to use simultaneousGesture. Just a simple onTapGesture() does the job.
Hmm. Unfortunately this is not working for me on XCode 15.1 and iOS 17. I am attempting to use the Menu inside the .toolbar.
If I use primaryAction in Xcode 16/iOS 18, the menu won't open. This is so annoying.
If you have a primary action, it overrides the tapping functionality, menu would open on long tap
3

Create a custom ButtonStyle and add a onChange to the configuration.isPressed:

Menu {
  …
} label: {
  …
}
.buttonStyle(OnPressButtonStyle {
  // Pressed
})

struct OnPressButtonStyle: ButtonStyle {
  var onPress: (() -> Void)

  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .onChange(of: configuration.isPressed) { oldValue, newValue in
        if let !oldValue, newValue {
          onPress()
        }
      }
  }
}

Comments

2

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")
}

1 Comment

How does it work? Primary action hijacks the opening of the menu… 🤔
2

I just want to bump this since Eric Horacek's answer works very well, and is probably the most version-agnostic method for doing this. It's actually the only way I've found to reliably trigger haptics, or any events, when a Menu opens.

    VStack {
        Menu {
            Button("Some Button", action: {})
        } label: {
           Text("Label")
        }
        .buttonStyle(OnPressButtonStyle(onPress: {
            filterHaptic.toggle()
        }))
    }
    .sensoryFeedback(.impact(flexibility: .rigid, intensity: 1.0), trigger: filterHaptic)



    struct OnPressButtonStyle: ButtonStyle {
      var onPress: (() -> Void)
      
      func makeBody(configuration: Configuration) -> some View {
        configuration.label
          .onChange(of: configuration.isPressed) { oldValue, newValue in
            if !oldValue && newValue {
              onPress()
            }
          }
      }
    }

2 Comments

Just in case .onChange(of: configuration.isPressed) { oldValue, newValue in is available only for iOS >= 17. Trying to find something that would work for iOS 16 as well
For iOS 16 we could use the old onChange call, with only one value, the value is boolean, only two values, and it looks like it triggers onChange correctly. configuration.label.onChange(of: configuration.isPressed) { value in if value { onPress() }})
1

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") }

1 Comment

Does it work in iOS 18 for you? It does not for me!
-1

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")
}                                 

2 Comments

onDisappear does not work here
What's the reason for misleading readers? onDisappear is not being triggered
-2

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

1 Comment

Thanks, but that's not the structure I'm trying to use. You're proposing a solution for the case when there's a button, which in turn shows or hided subviews. I would assume that would work fine, because views actually are getting built and destroyed as a part of the process (and trigger the onAppear) action. I'm trying to get haptic feedback for actual menu structure, as described here: developer.apple.com/documentation/swiftui/menu If I put the onAppear handler on the Menu() structure itself, it just fires once when menu trigger gets into the view as a part of parent view.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.