Adding a navigation link inside a context menu - ios

This question is based on my previous two questions but I am asking it because when I responded to those answers I didn't get a response that helped. I have gone through multiple answers online and people seem to have a similar issue where a navigation link wouldn't work in a context menu.
#State private var selectedBook: Book? = nil
//...other code
ForEach(bookData){ bookDetail in
BookView(book: bookDetail)
.background(NavigationLink(destination: EditBook(book: bookDetail), tag: bookDetail, selection: $selectedBook){ EmptyView() })
.contextMenu{
Button(action: {
self.selectedBook = bookDetail
}) {
Label("Edit", systemImage: "pencil")
}
Button(action: {
//Delete action
}) {
abel("Delete", systemImage: "trash")
}
}
Basically in my first question I had the navigation link under the first button which wasn't and the solution was using a bool #State variable and the .background modifier but then the link wasn't passing the right view because of the for each (second question) and now I have arrived on this code.
My problem is that the .background modifier opens the view in the navigation link on the tap of the view it's bound to as well as the tap of the context menu. I need the tap action to open a different view so I want the current nav link to open only at a press of the context menu.

Can you try putting the BookView into a ZStack and just putting the Navigation Link inside? Instead of your BookView & .background, something like:
ZStack {
NavigationLink(destination: EditBook(book: bookDetail), isActive: $selectedBook, label: { EmptyView() })
BookView(book: bookDetail)
}

A derivative of my first answer should work. Here's a more complete version:
struct TestView: View {
#State private var selectedBook: String = ""
#State private var showSelectedBookView: Bool = false
let books = [
"BOOK1",
"BOOK2",
"BOOK3",
"BOOK4",
"BOOK5",
"BOOK6",
"BOOK7",
]
var body: some View {
ZStack {
NavigationLink(destination: Text("SELECTED BOOK IS: \(selectedBook)"), isActive: $showSelectedBookView, label: { EmptyView() })
VStack(spacing: 20) {
ForEach(books, id: \.self) { book in
Text(book)
.contextMenu(ContextMenu(menuItems: {
Button(action: {
self.selectedBook = book
self.showSelectedBookView.toggle()
}, label: {
Label("Edit", systemImage: "pencil")
})
Button(action: {
}, label: {
Label("Delete", systemImage: "trash")
})
}))
}
}
}
}
}

Related

SwiftUI Picker and Buttons inside same Form section are triggered by the same user click

I have this AddWorkoutView and I am trying to build some forms similar to what Apple did with "Add new contact" sheet form.
Right now I am trying to add a form more complex than a simple TextField (something similar to "add address" from Apple contacts but I am facing the following issues:
in the Exercises section when pressing on a new created entry (exercise), both Picker and delete Button are triggered at the same time and the Picker gets automatically closed as soon as it gets open and the selected entry is also deleted when going back to AddWorkoutView.
Does anyone have any idea on how Apple implemented this kind of complex form like in the screenshow below?
Thanks to RogerTheShrubber response here I managed to somehow implement at least the add button and to dynamically display all the content I previously added, but I don't know to bring together multiple TextFields/Pickers/any other stuff in the same form.
struct AddWorkoutView: View {
#EnvironmentObject var workoutManager: WorkoutManager
#EnvironmentObject var dateModel: DateModel
#Environment(\.presentationMode) var presentationMode
#State var workout: Workout = Workout()
#State var exercises: [Exercise] = [Exercise]()
func getBinding(forIndex index: Int) -> Binding<Exercise> {
return Binding<Exercise>(get: { workout.exercises[index] },
set: { workout.exercises[index] = $0 })
}
var body: some View {
NavigationView {
Form {
Section("Workout") {
TextField("Title", text: $workout.title)
TextField("Description", text: $workout.description)
}
Section("Exercises") {
ForEach(0..<workout.exercises.count, id: \.self) { index in
HStack {
Button(action: { workout.exercises.remove(at: index) }) {
Image(systemName: "minus.circle.fill")
.foregroundColor(.red)
.padding(.horizontal)
}
Divider()
VStack {
TextField("Title", text: $workout.exercises[index].title)
Divider()
Picker(selection: getBinding(forIndex: index).type, label: Text("Type")) {
ForEach(ExerciseType.allCases, id: \.self) { value in
Text(value.rawValue)
.tag(value)
}
}
}
}
}
Button {
workout.exercises.append(Exercise())
} label: {
HStack(spacing: 0) {
Image(systemName: "plus.circle.fill")
.foregroundColor(.green)
.padding(.trailing)
Text("add exercise")
}
}
}
}
.navigationTitle("Create new Workout")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button {
presentationMode.wrappedValue.dismiss()
} label: {
Text("Cancel")
}
.accessibilityLabel("Cancel adding Workout")
}
ToolbarItem(placement: .confirmationAction) {
Button {
} label: {
Text("Done")
}
.accessibilityLabel("Confirm adding the new Workout")
}
}
}
}
}

SwiftUI List selection doesn’t show If I add a NavigationLink and a .contextMenu to the list. Is this a known bug?

List selection doesn’t show If I add a NavigationLink and a .contextMenu to the list, when I select a row, the selection disappears.
struct ContentView: View {
#State private var selection: String?
let names = ["Cyril", "Lana", "Mallory", "Sterling"]
var body: some View {
NavigationView {
List(names, id: \.self, selection: $selection) { name in
NavigationLink(destination: Text("Hello, world!")) {
Text(name)
.contextMenu {
Button(action: {}) {
Text("Tap me!")
}
}
}
}
.toolbar {
EditButton()
}
}
}
}
We can disable context menu button(s) for the moment of construction in edit mode (because the button is a reason of issue).
Here is a possible approach - some redesign is required to handle editMode inside context menu (see also comments inline).
Tested with Xcode 13.2 / iOS 15.2
struct ContentViewSelection: View {
#State private var selection: String?
let names = ["Cyril", "Lana", "Mallory", "Sterling"]
var body: some View {
NavigationView {
List(names, id: \.self, selection: $selection) { name in
// separated view is needed to use editMode
// environment value
NameCell(name: name)
}
.toolbar {
EditButton()
}
}
}
}
struct NameCell: View {
#Environment(\.editMode) var editMode // << !!
let name: String
var body: some View {
NavigationLink(destination: Text("Hello, world!")) {
Text(name)
}
.contextMenu {
if editMode?.wrappedValue == .inactive { // << !!
Button(action: {}) {
Text("Tap me!")
}
}
}
}
}

How do I navigate to another SwiftUI View when an Environment Object update is causing the same view to reload

What I'm trying to achieve: I'm a new SwiftUI developer. I'm trying to build a simple Address Book app. I have three views:
ContentView - The main view which contains all contacts in a List View with an Add Contact ('+') and Edit button at the top of the Navigation View
AddContact View - which has a "Name" and "Email" text field and a "Submit" button
DisplayContactDetails View - not relevant to this question.
I've created an Environment Object "myContacts" which is an array of "Contact" objects and passed it in the ContentView to keep track of all contacts in the Address Book
When the user navigates to AddContact View, adds a name and email and submits, I'd like the Environment Object "myContacts" to be updated and for the user to be navigated back to ContentView so they can see the Address Book with the new contact included.
Problem:
When the user presses "Submit" on AddContact View, it correctly invokes a navigation link I've created to send the user back to ContentView. But because the Environment Object "myContacts" has also been updated by submit, it immediately navigates back from ContentView to AddContact View again. So it appears to be executing the Navigation Link first but then reloading AddContact View due to the refresh of myContacts.
Code - Content view:
struct ContentView: View {
#EnvironmentObject var myContacts: Contacts
#State var isAddButtonPressed: Bool = false
var body: some View {
NavigationView{
List {
ForEach(myContacts.contacts) { item in
NavigationLink(
//Display items and send user to DisplayContactDetails
})
}
}
.navigationBarTitle("Address Book")
.toolbar {
ToolbarItem(placement: .navigationBarLeading){
Button(action: {
isAddButtonPressed.toggle()
}, label: {
NavigationLink(
destination: AddContactView(),
isActive: $isAddButtonPressed,
label: {
Image(systemName: "plus")
})
})
}
ToolbarItem(placement: .navigationBarTrailing){
EditButton()
}
}
}
}
}
Code - AddContactView
struct AddContactView: View {
#State var name: String = ""
#State var email: String = ""
#State var isButtonPressed: Bool = false
#EnvironmentObject var myContacts: Contacts
var body: some View {
VStack{
HStack{
Text("Name:")
TextField("Enter name", text: $name)
}
.padding(.bottom, 50)
HStack{
Text("Email:")
TextField("Enter email", text: $email)
}
.padding(.bottom, 50)
Button("Submit") {
let contactToAdd = Contact(name: name, email: email)
//Add is a simple function - all it does is append an item to the myContacts array using the .append method
myContacts.add(contact: contactToAdd)
isButtonPressed = true
}
.frame(width: 80, height: 30, alignment:.center)
.background(Color.blue)
.foregroundColor(.white)
.clipShape(Capsule())
NavigationLink(destination: ContentView().navigationBarHidden(true),
isActive: $isButtonPressed,
label: {
EmptyView()
}).hidden()
}.padding()
}
}
What I've tried
If I comment out the the .add method and don't update the environment object, then the navigation back to ContentView works as expected. So I know that specifically is the cause of the problem.
I've tried adding a .onTapGesture modifier to the Button and invoking .add there.
I've tried adding a .onDisappear modifier to the entire view and invoking .add there.
--
Any help or clarity on resolving this would be much appreciated
Edit: Screen Recording - trying solution based on first comment:
What happens when I try the solution
Odd behaviour: The first attempt at adding a contact auto-routes back to AddContactView, producing the same error. But if I try it a second time then it routes correctly to ContactView.
Edit update. This is the code I used to test my answer:
import SwiftUI
#main
struct TestApp: App {
#StateObject var myContacts = Contacts()
var body: some Scene {
WindowGroup {
ContentView().environmentObject(myContacts)
}
}
}
struct Contact: Identifiable {
var id = UUID()
var name: String = ""
var email: String = ""
}
class Contacts: ObservableObject {
#Published var contacts: [Contact] = [Contact(name: "name1", email: "email1"), Contact(name: "name2", email: "email2")]
func add(contact: Contact) {
contacts.append(contact)
}
}
struct AddContactView: View {
#Environment(\.presentationMode) private var presentationMode
#EnvironmentObject var myContacts: Contacts
#State var name: String = ""
#State var email: String = ""
var body: some View {
VStack{
HStack{
Text("Name:")
TextField("Enter name", text: $name)
}
.padding(.bottom, 50)
HStack{
Text("Email:")
TextField("Enter email", text: $email)
}
.padding(.bottom, 50)
Button("Submit") {
let contactToAdd = Contact(name: name, email: email)
myContacts.add(contact: contactToAdd)
presentationMode.wrappedValue.dismiss()
}
.frame(width: 80, height: 30, alignment:.center)
.background(Color.blue)
.foregroundColor(.white)
.clipShape(Capsule())
}.padding()
}
}
struct ContentView: View {
#EnvironmentObject var myContacts: Contacts
#State var isAddButtonPressed: Bool = false
var body: some View {
NavigationView {
List {
ForEach(myContacts.contacts) { item in
NavigationLink(destination: AddContactView()) {
Text(item.name)
}
}
}
.navigationBarTitle("Address Book")
.toolbar {
ToolbarItem(placement: .navigationBarLeading){
Button(action: {
isAddButtonPressed.toggle()
}, label: {
NavigationLink(
destination: AddContactView(),
isActive: $isAddButtonPressed,
label: {
Image(systemName: "plus")
})
})
}
ToolbarItem(placement: .navigationBarTrailing){
EditButton()
}
}
}
}
}

SwiftUI: How to display the second Sheet when first sheet is closed

I want to make a side menu with .fullScreenCover, when a user is logged in and press MyGarage button. the .fullScreenCover will dismiss and the main view will navigate to MyGarage View. But if user is not logged in the .fullScreenCover will dismiss and a loginView with .fullScreenCover will appear. My problem is, the .fullScreenCover will not work if I put 2 same .fullScreenCover inside the main view. Is there any way to solve this? I'm sorry it's a little bit difficult for me to explain.
Here's the code
SideMenuView
struct SideMenuView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var showMyGarage: Bool
#Binding var showSignIn: Bool
var user = 0 //If user is 1, it is logged in
var body: some View {
NavigationView{
VStack{
Button(action: {
presentationMode.wrappedValue.dismiss()
if user == 1 {
self.showMyGarage = true
}else{
self.showSignIn = true
}
}, label: {
Text("My Garage")
})
}
.navigationBarTitleDisplayMode(.inline)
.navigationBarItems(leading:
HStack(spacing: 20){
Button(action: {
presentationMode.wrappedValue.dismiss()
}, label: {
Text("X")
})
Text("Main Menu")
}
)
}.navigationViewStyle(StackNavigationViewStyle())
}
}
MainView
struct HomeView: View {
#State var showSideMenu = false
#State private var showMyGarage = false
#State var showSignIn = false
var body: some View {
VStack{
Text("Home")
NavigationLink(destination: MyGarageView(showMyGarage: $showMyGarage), isActive: $showMyGarage){
EmptyView()
}
}
.navigationBarItems(leading:
Button(action: {
self.showSideMenu.toggle()
}, label: {
Text("Menu")
})
)
.fullScreenCover(isPresented: $showSideMenu, content: {
SideMenuView(showMyGarage: $showMyGarage, showSignIn: $showSignIn)
})
.fullScreenCover(isPresented: $showSignIn, content: {
SignInView()
})
}
}
struct MyGarageView: View {
#Binding var showMyGarage: Bool
var body: some View {
Text("MyGarage")
}
}
struct SignInView: View {
var body: some View {
Text("Sign In")
}
}
Try to attach them to different views, like
var body: some View {
VStack{
Text("Home")
.fullScreenCover(isPresented: $showSideMenu, content: {
SideMenuView(showMyGarage: $showMyGarage, showSignIn: $showSignIn)
})
NavigationLink(destination: MyGarageView(showMyGarage: $showMyGarage), isActive: $showMyGarage){
EmptyView()
}
.fullScreenCover(isPresented: $showSignIn, content: {
SignInView()
})
}
.navigationBarItems(leading:
Button(action: {
self.showSideMenu.toggle()
}, label: {
Text("Menu")
})
)
}

Transition animation gone when presenting a NavigationLink in SwiftUI

I have a List with NavigationLinks.
List {
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(), tag: item.id, selection: self.$viewModel.selectedItemId) {
Text("Some text")
}
}
.onDelete(perform: delete)
}
.id(UUID())
And a corresponding ViewModel which stores the selected item's id.
class ViewModel: ObservableObject {
#Published var selectedItemId: String? {
didSet {
if let itemId = selectedItemId {
...
}
}
}
...
}
The problem is that when I use NavigationLink(destination:tag:selection:) the transition animation is gone - the child view pops up immediately. When I use NavigationLink(destination:) it works normally, but I can't use it because I need to perform some action when a NavigationLink is selected.
Why the transition animation is gone? Is this a problem with NavigationLink(destination:tag:selection:)?
You could put your action inside the NavigationLink. This should solve your animation problem:
List {
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(), isActive: $isActive, tag: item.id, selection: self.$viewModel.selectedItemId) {
EmptyView()
}
Button(action: {
// The action you wand to have done, when the View pops.
print("Test")
self.isActive = true
}, label: {
Text("Some text")
})
}
.onDelete(perform: delete)
}
.id(UUID())
It turned out to be a problem with .id(UUID()) at the end of the list. Removing it restores the transition animation:
List {
ForEach(items, id: \.id) { item in
NavigationLink(destination: ItemView(), tag: item.id, selection: self.$viewModel.selectedItemId) {
Text("Some text")
}
}
.onDelete(perform: delete)
}
//.id(UUID()) <- removed this line
I added this following this link How to fix slow List updates in SwiftUI. Looks like this hack messes up tags/selections when using NavigationLink(destination:tag:selection:).

Resources