How to Configure Contextmenu Buttons For Delete and Disabled in Swiftui

How to configure ContextMenu buttons for delete and disabled in SwiftUI?

All of the asked situations are now supported in iOS 15

Destructive: (works from iOS 15)

Set .destructive as the role argument of the button:

Button(role: .destructive) { // This argument
// delete something
} label: {
Label("Delete", systemImage: "trash")
}

Delete Demo



Disabled: (works from iOS 14.2)

Add .disabled modifier to the button.

Button {
// call someone
} label: {
Label("Call", systemImage: "phone")
}.disabled(true) // This modifier

Disabled Demo



Divider: (works from iOS 14)

Use a Divider() view directly.

Divider Demo



Full Demo:

Demo
⚠️ Remember! Do not use image instead of systemImage for showing an SFSymbol on the button!

SwiftUI contextmenu wrong location

The problem is that your code applies the contextMenu modifier after the position modifier.

Let's consider this slightly modified example:

ZStack {
GeometryReader { geometry in
ForEach(self.document.instruments, id: \.id) { instrument in
Image(instrument.text)
.frame(width: 140, height: 70)
.position(self.position(for: instrument, in: geometry.size))
.contextMenu { ... }
}
}
}

In the SwiftUI layout system, a parent view is responsible for assigning positions to its child views. A view modifier acts as the parent of the view it modifies. So in the example code:

  • ZStack is the parent of GeometryReader.
  • GeometryReader is the parent of ForEach.
  • ForEach is the parent of contextMenu.
  • contextMenu is the parent of position.
  • position is the parent of frame.
  • frame is the parent of Image.
  • Image is not a parent. It has no children.

(Sometimes the parent is called the “superview” and the child is called the “subview”.)

When contextMenu needs to know where to draw the menu on the screen, it looks at the position given to it by its parent, the ForEach, which gets it from the GeometryReader, which gets it from the ZStack.

When Image needs to know where to draw its pixels on the screen, it looks at the position given to it by its parent, which is the frame modifier, and the frame modifier gets the position from the position modifier, and the position modifier modifies the position given to it by the contextMenu.

This means that the position modifier does not affect where the contextMenu draws the menu.

Now let's rearrange the code so contextMenu is the child of position:

ZStack {
GeometryReader { geometry in
ForEach(self.document.instruments, id: \.id) { instrument in
Image(instrument.text)
.frame(width: 140, height: 70)
.contextMenu { ... }
.position(self.position(for: instrument, in: geometry.size))
}
}
}

Now the contextMenu gets its position from the position modifier, which modifies the position given to it by the ForEach. So in this scenario, the position modifier does affect the where the contextMenu draws the menu.

Context Menu Destructive Actions, SwiftUI

I've played with different things: fg, bg, accent etc but to no avail. I'm guessing "not possible" at the moment.

ps ContextMenu is deprecated, but not .contextMenu
You're doing it right.

This is the non-deprecated one. The other one doesn't use a ViewBuilder

public func contextMenu<MenuItems>(@ViewBuilder menuItems: () -> MenuItems) -> some View where MenuItems : View

Make a .contextMenu update with changes from PasteBoard

You can listen to UIPasteboard.changedNotification to detect changes and refresh the view:

struct ContentView: View {
@State private var pasteDisabled = false

var body: some View {
Text("Some Text")
.contextMenu {
Button(action: {}) {
Text("Paste")
Image(systemName: "doc.on.clipboard")
}
.disabled(pasteDisabled)
}
.onReceive(NotificationCenter.default.publisher(for: UIPasteboard.changedNotification)) { _ in
pasteDisabled = !UIPasteboard.general.contains(pasteboardTypes: [aPAsteBoardType])
}
}
}

(You may also want to use UIPasteboard.removedNotification).

SwiftUI destructive Button style

For Button:

Button("Tap") {
// do something
}
.foregroundColor(.red)

For Alert:

Alert(
title: Text("Hi"),
message: Text("Do it?"),
primaryButton: .cancel(Text("Cancel")),
secondaryButton: .destructive(Text("Delete")) {
// do something
}
)

And similarly for ActionSheet.

SwiftUI: How to ignore taps on background when menu is open?

You can implement an .overlay which is tappable and appears when you tap on the menu.
Make it cover the whole screen, it gets ignored by the Menu.
When tapping on the menu icon you can set a propertie to true.
When tapping on the overlay or a menu item, set it back to false.

You can use place it in your root view and use a viewmodel with @Environment to access it from everywhere.

The only downside is, that you need to place isMenuOpen = false in every menu button.

Apple is using the unexpected behaviour itself, a.ex in the Wether app.
However, I still think it's a bug and filed a report. (FB10033181)

Sample Image

@State var isMenuOpen: Bool = false

var body: some View {
NavigationView{
NavigationLink{
ChildView()
} label: {
Text("Some NavigationLink")
.padding()
}
.toolbar{
ToolbarItem(placement: .navigationBarTrailing){
Menu{
Button{
isMenuOpen = false
} label: {
Text("Some Action")
}
} label: {
Image(systemName: "ellipsis.circle")
}
.onTapGesture {
isMenuOpen = true
}
}
}
}
.overlay{
if isMenuOpen {
Color.white.opacity(0.001)
.ignoresSafeArea()
.frame(maxWidth: .infinity, maxHeight: .infinity)
.onTapGesture {
isMenuOpen = false
}
}
}
}


Related Topics



Leave a reply



Submit