SwiftUI Menu - how to fire action when clicking on menu with submenus - swiftui

If I define a Menu with submenus, how do I register a tap when clicking on the menu with submenus?
With the code below, I would like to "print" when clicking on the "menu with submenus". But this is not fired. Instead, the menu opens and displays it's submenus, which is fine. I would just like to add custom code when opening those submenus.
struct ContentView: View {
var body: some View {
Menu {
Button {
print("sub1")
} label: {
Text("sub1")
}
Menu {
Button {
print("sub2")
} label: {
Text("sub2")
}
} label: {
Button {
print("menu with submenus")
} label: {
Text("menu with submenus")
}
}
} label: {
Button {
print("opening menu")
} label: {
Text("open menu")
}
}
}
}

Related

SwiftUI toolbar button item issue

I have a ToolbarItemGroup with a navigation link and a button. With the current setup when I hit "Add" it goes to the AddNewTripView just fine but when I hit the back button (to return to the list view) it goes back to the previous view and then jumps right back to the AddNewTripView.
If I remove the Edit button it works fine. Is this a bug in SwiftUI or am I doing something wrong?
var body: some View {
ScrollView { }
.onAppear {
tripListViewModel.getAllTrips()
}
.navigationTitle("Trips")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
NavigationLink(destination: AddNewTripView(repo: repo)) {
Text("Add")
}
Button(action: { isEditing.toggle() }) {
Text(isEditing ? "Done" : "Delete")
.foregroundColor(.primary)
}
}
}
}

Swiftui how to catch taps on menu item

I'm trying to catch the tap on an item in a menu, but I can't get it to work.
I've tried something like this:
Menu("Tap me") {
ForEach (references, id: \.self) { ref in
Link(ref.name, destination: URL(string: ref.url))
.simultaneousGesture(TapGesture().onEnded{
onLinkTapped()
})
.onTapGesture{
onLinkTapped()
}
}
}
but I never get to the onLinkTapped(). If it's not inside a Menu, then it works, but not inside the menu. How to catch the tap gesture?
Just use Button inside Menu:
Menu("Tap me") {
ForEach (references, id: \.self) { ref in
Button {
onLinkTapped()
} label: {
Link(ref.name, destination: URL(string: ref.url))
}
}
}

How do I get an "edit" button for a row in a list/for each loop? [SwiftUI]

I want to get an edit and add a button like the circled buttons shown in the image below. I need these buttons so that I can change the properties of the folders. How do I get this button to work when the default SwiftUI Edit button is clicked?
What I want:
(Before Edit button clicked)
(After Edit Button Clicked)
What I currently have:
(Before Edit button clicked)
(After Edit Button Clicked)
My code:
var body: some View {
ZStack {
List {
ForEach(searchResults, id: \.self.id) { dir in
Section(dir.title) {
ForEach(dir.getChildFolders(), id: \.self.id) { folder in
NavigationLink(destination: DirectoryView(directory: folder)) {
Label(folder.title, systemImage: "folder")
}
}
.onDelete(perform: { offsets in
dir.items.remove(atOffsets: offsets)
updateView.update()
})
.onMove(perform: { source, destination in
dir.items.move(fromOffsets: source, toOffset: destination)
updateView.update()
})
}
}
}
You need to display the button yourself.
First, ensure you have some #State var tracking whether Edit mode is toggled:
#State var editing: Bool = false
Then, in your body, show the circle button next to the NavigationLink if you are editing:
var body: some View {
ZStack {
List {
ForEach(searchResults, id: \.self.id) { dir in
Section(dir.title) {
ForEach(dir.getChildFolders(), id: \.self.id) { folder in
HStack {
NavigationLink(destination: DirectoryView(directory: folder)) {
Label(folder.title, systemImage: "folder")
}
if editing {
Spacer()
Button(action: {
// Perform circle button action here
}) {
Image(systemName: "ellipsis.circle")
}
}
}
}
.onDelete(perform: { offsets in
dir.items.remove(atOffsets: offsets)
updateView.update()
})
.onMove(perform: { source, destination in
dir.items.move(fromOffsets: source, toOffset: destination)
updateView.update()
})
}
}
}
}
}

Add button to picker label

I want to add a button to swiftui picker's label.
But the button is not clickable.
When I click on the button the picker is clicked.
How Do I make the picker take clicks only in the area of the selected value?
and the buttons take his clicks?
import SwiftUI
enum Animal: String, CaseIterable, Identifiable {
case dog
case cat
case bird
var id: String { self.rawValue }
}
struct ContentView: View {
#State private var selectedAnimal = Animal.dog
var body: some View {
Form {
Group {
Section(header: Text("Animales")) {
VStack{
Picker(
selection: $selectedAnimal,
content: {
ForEach(Animal.allCases, id:\.self) {
Text($0.rawValue)
}},
label: {
HStack {
Text ("Chose Animale")
Spacer ()
Button (
action: {
print ("clicked")
},
label: {
Image(systemName: "arrow.clockwise")
})
Spacer ()
}
}
)
}
}
}
}
}
}
To solve this issue we need to separate picker and button and block Form tracking click inside row (which is by default track entire row).
For first move button out of picker and place everything in HStack, for second we need couple of tricks like tapGesture on label and non-default button style for button (for simplicity I used primitive button style, but it's better to create custom with appropriate highlight, etc.)
Here is a simplified updated and tested your code (Xcode 13 / iOS 15):
var body: some View {
Form {
Group {
Section(header: Text("Animales")) {
HStack{
HStack {
Text ("Chose Animale")
Spacer ()
}
.contentShape(Rectangle())
.onTapGesture {
// just blocker for label click
}
.overlay(
Button (
action: {
print ("clicked")
},
label: {
Image(systemName: "arrow.clockwise").foregroundColor(.blue)
})
.buttonStyle(PlainButtonStyle()) // << needed custom !!
)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.layoutPriority(1) // << to cover much area
//.border(Color.red) // << for testing area
Picker("",
selection: $selectedAnimal,
content: {
ForEach(Animal.allCases, id:\.self) {
Text($0.rawValue)
}}
)
.labelsHidden() // << hide own label
.fixedSize() // << limit size !!
}
.listRowInsets(EdgeInsets()) // << consume row space !!
}
}
}
}

Multiple sheet(item: ) triggered by SwipeActions button and ToolBar buttons gets nil object in the first time in SwiftUI

I am using swipeActions in ForEach loop of a List and toolbar buttons to show different sheets on my SwiftUI view. But every first time I swipe left and click the Edit button, the object of that line is nil. If I do the same swipe and click thing again, everything goes well. Anyone else had this kind of bug before? Thank you.
Here is the related code:
struct LanguagesView: View {
#State var activeSheet: ActiveSheet?
#State var toBeEdit: MeLanguage?
var body: some View {
NavigationView {
List {
ForEach(self.meLanguages, id: \.id) { lan in
HStack {
Text("\(lan.wrappedName)")
.font(.headline)
}.swipeActions(allowsFullSwipe: false) {
Button(
action: {
self.activeSheet = .editLanguage
self.toBeEdit = lan
},
label: { Label("Edit", systemImage: "pencil") }
) .tint(.indigo)
}
}
}
.sheet(item: $activeSheet,
onDismiss: {
self.toBeEdit = nil
}
){
item in
switch item {
case .addLanguage:
AddLanguage()
case .sortLanguages:
SortLanguagesView()
case .editLanguage:
if self.toBeEdit != nil {
EditLanguageView( meLanguage: self.toBeEdit! )
}
else {
Text("self.toBeEdit is nil")
}
default:
Text("No such button on ContentView.")
}
}
.toolbar {
ToolbarItemGroup {
HStack {
Text("\(self.meLanguages.count) Languages on Card").font(.headline)
self.barButtons
}
}
}
}
}
var barButtons: some View {
HStack {
Button(
action: {
self.activeSheet = .sortLanguages
},
label: { Label("Sort Languages", systemImage: "arrow.up.arrow.down.circle")
}
).id("sortLanguages")
Button(
action: {
self.activeSheet = .addLanguage
},
label: { Label("Add Language",
systemImage: "plus")
.imageScale(.large)
}
)
}
}
}
If I only think of the sheet triggered by swipeActions Edit button, the code below works perfectly. But I still need other sheets triggered by ToolBar buttons.
.sheet(item: self.$toBeEdit, content: { toBeEdit in
EditLanguageView( meLanguage: toBeEdit)
})
After more searching I realised it's not something about SwipeActions. This is actually similar to this question:
SwiftUI presenting sheet with Binding variable doesn't work when first presented
So I added an hidden Text after the ForEach loop and the problem solved.
ForEach(self.meLanguages, id: \.id) { ... }
if self.toBeEdit != nil {
Text("\(self.toBeEdit!.wrappedName)").hidden()
}