13

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.
Mikhail Kornienko
  • 1,162
  • 1
  • 8
  • 12

3 Answers3

4

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.

Asperi
  • 228,894
  • 20
  • 464
  • 690
0

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

Simon
  • 1,754
  • 14
  • 32
  • 4
    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: https://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. – Mikhail Kornienko Mar 20 '21 at 12:42
0

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")
}                                 
Cherpak Evgeny
  • 2,659
  • 22
  • 29