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")
}
Disabled: (works from iOS 14.2)
Add .disabled
modifier to the button.
Button {
// call someone
} label: {
Label("Call", systemImage: "phone")
}.disabled(true) // This modifier
Divider: (works from iOS 14)
Use a Divider()
view directly.
Full 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 ofGeometryReader
.GeometryReader
is the parent ofForEach
.ForEach
is the parent ofcontextMenu
.contextMenu
is the parent ofposition
.position
is the parent offrame
.frame
is the parent ofImage
.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)
@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
Should Conditional Compilation Be Used to Cope With Difference in Cgfloat on Different Architectures
Swift 2.1 [Uint8] --Utf8--> String
Init(Start:End:)' Is Deprecated: It Will Be Removed in Swift 3. Use the '..<' Operator
Draw on a PDF Using Swift on MACos
Handling Multiple Gesturerecognizers
Using Combine's Future to Replicate Async Await in Swift
Swift Turn a Country Code into a Emoji Flag via Unicode
How to Require That a Protocol Can Only Be Adopted by a Specific Class
How to Add Material to Modelentity Programatically in Realitykit
Swift Spritekit Adding Button Programmatically
Swift: How to Detect Linear Type Barcodes
Swift: Declaration in Generic Class
Show Am/Pm in Capitals in Swift
Class 'Viewcontroller' Has No Initializers in Swift
Error When Trying to Save Image in Nsuserdefaults Using Swift
Firebase: How to Update Multiple Nodes Transactionally? Swift 3