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

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")
}
}
}
}
}

Related

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")
})
)
}

Is there a problem with the way I'm structuring my SwiftUI project that causes .navigationTitle to not appear?

My SwiftUI project refuses to display the navigation title after a certain point. I'm using a navigation structure that I haven't seen implemented on any example projects I've seen, but it makes sense to me and has been seeming to work thus far. My suspicion is that since I'm using a different navigation structure that it is part of the problem. The .navigationBar is there on every page, but the title doesn't display.
SettingsView.swift screen
I've tried many solutions on Stack Overflow and otherwise. I've tried every combination of .navigationBarHidden(false), .navigationBarTitle() and .navigationBarBackButtonHidden(true) on every page listed below, with no change. I've also tried every location I could think of to place these combinations of .navigationBar modifiers.
Recently, I discovered .toolbar, and this changes nothing either. My suspicion is that (as seen in the code snippets below) since the NavigationView is in the first view (WelcomeUI.swift), I can't place the .navigationBarTitle deeper in the code.
Below is my current navigation structure, and bits of code from each file:
WelcomeUI.swift
struct WelcomeUI: View {
var body: some View {
NavigationView {
VStack {
//NavigationLink(destination: SignupUI(), label: {
//Text("Sign Up")
//}
NavigationLink(destination: LoginUI(), label: {
Text("Log In")
}
}
}
}
}
LoginUI.swift
struct LoginUI: View {
var body: some View {
VStack {
NavigationLink(destination: MainUI(), label: { Text("Log In") })
//Button(action: { ... }
}
.navigationBarHidden(false)
}
}
Note: SignupUI.swift is essentially the same as LoginUI.swift
MainUI.swift
struct MainUI: View {
var body: some View {
TabView {
//SpendingView()
//.tabItem {
//Image(...)
//Text("Spending")
//}
//SavingView()
//.tabItem {
//Image(...)
//Text("Saving")
//}
//AddView()
//.tabItem {
//Image(...)
//Text("Add")
//}
//EditView()
//.tabItem {
//Image(...)
//Text("Edit")
//}
SettingsView()
.tabItem {
//Image(...)
Text("Settings")
}
}
.navigationBarBackButtonHidden(true)
}
}
Note: All views in MainUI.swift are structured the same.
SettingsView.swift
struct SettingsView: View {
var body: some View {
ZStack {
Form {
Section(header: Text("Section Header")) {
NavigationLink(destination: WelcomeUI()) {
Text("Setting Option")
}
}
Section {
//Button("Log Out") {
//self.logout()
//}
Text("Log Out")
}
}
.navigationBarTitle("Settings") // This has no effect on code no matter where it is place in SettingsView.swift
}
}
}
I should also note that only the pages after MainUI.swift is affected by this. WelcomeUI.swift and LoginUI.swift work as expected.
Look at the MainUI navigationTitle. I just put a title in every View to find what was going on.
import SwiftUI
struct SubSpendingView: View {
var body: some View {
ScrollView{
Text("SubSpendingView")
}.navigationBarTitle("SubSpending"
//, displayMode: .inline
)
}
}
struct SpendingView: View {
var body: some View {
ScrollView{
Text("SpendingView")
NavigationLink("subSpending", destination: SubSpendingView())
}.padding()
}
}
struct WelcomeUI: View {
var body: some View {
NavigationView {
VStack {
//NavigationLink(destination: SignupUI(), label: {
//Text("Sign Up")
//}
NavigationLink(destination: LoginUI(), label: {
Text("Go to Log In")
})
}.navigationTitle(Text("WelcomeUI"))
}
}
}
struct SettingsView: View {
var body: some View {
VStack{
ZStack {
Form {
Section(header: Text("Section Header")) {
NavigationLink(destination: WelcomeUI()) {
Text("Setting Option")
}
}
Section {
//Button("Log Out") {
//self.logout()
//}
Text("Log Out")
}
}
Button("say-high", action: {print("Hi")})
// This has no effect on code no matter where it is place in SettingsView.swift
}
}//.navigationBarTitle("Settings")
}
}
struct LoginUI: View {
var body: some View {
VStack {
NavigationLink(destination: MainUI(), label: { Text("Log In") })
//Button(action: { ... }
}.navigationTitle(Text("LoginUI"))
.navigationBarHidden(false)
}
}
struct MainUI: View {
#State var selectedTab: Views = .adding
var body: some View {
TabView(selection: $selectedTab) {
SpendingView()
.tabItem {
Image(systemName: "bag.circle")
Text("Spending")
}.tag(Views.spending)
//SavingView()
//.tabItem {
//Image(...)
//Text("Saving")
//}
Text("Adding View")
.tabItem {
Image(systemName: "plus")
Text("Add")
}.tag(Views.adding)
Text("Edit View")
.tabItem {
Image(systemName: "pencil")
Text("Edit")
}.tag(Views.edit)
SettingsView()
.tabItem {
Image(systemName: "gear")
Text("Settings")
}.tag(Views.settings)
}
//This overrides the navigationTitle in the tabs
.navigationBarTitle(Text(selectedTab.rawValue)
//, displayMode: .inline
)
.navigationBarBackButtonHidden(true)
}
}
enum Views: String{
case settings = "Settings"
case spending = "Spending"
case adding = "Add"
case edit = "Edit"
}
struct PaddyNav: View {
var body: some View {
WelcomeUI()
//Wont work
.navigationTitle(Text("PaddyNav"))
}
}
struct PaddyNav_Previews: PreviewProvider {
static var previews: some View {
PaddyNav()
}
}

Adding a navigation link inside a context menu

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")
})
}))
}
}
}
}
}

Resources