I am trying to make a popup menu (favorite menu), and I am stuck when I try to update my detail menu popup screen.
Here, when I double-tapped one of my Home() list objects, the popup menu is popup. From that popup menu, the user will select one of the two buttons. However, every time I tap on the list object, the popup menu shows only the first name of the first array. It looks like the Double Tap Gesture is not updated. It stuck in the first array although I tried to tap one a different list object. Please help me how to update my popup screen based on the selected list object. Also, NavigationLink might be the solution, but I don't want to use NavigationLink. I just want to toggle the screen.
All the detailed code examples are as below.
This is my sample array:
import SwiftUI
struct DataArray: Identifiable {
let id = UUID()
let number: Int
let cities: String
var name1: String?
var name2: String?
var name3: String?
var name4: String?
}
public struct ListDataArray {
static var dot = [
DataArray(number: 1,
cities: "Baltimore"
name1: "A",
name2: "B"),
DataArray(number: 2,
cities: "Frederick"),
DataArray(number: 3,
cities: "Catonsville"
name1: "Aa",
name2: "Bb",
name3: "Cc",
name4: "Dd"),
]
}
This is for my Double Tab Gesture:
struct Home: View {
#EnvironmentObject var tap: Tapping
var datas: [DataArray] = ListDataArray.dot
var body: some View {
ScrollView {
LazyVStack(spacing: 10) {
ForEach (data, id: \.id) { data in
if let data1 = data.name1 {
Text(data1)
}
if let city1 = data.cities {
Text(city1)
}
if let data2 = data.name2 {
Text(data2)
}
}
.onTapGesture (count: 2) {
self.tap.isDoubleTab = true
}
}
}
}
}
Finally, this is my target popup menu:
struct DetailMenu: View {
var dataArr = DataArray
#EnvironmentObject var tap: Tapping
var body: some View {
VStact {
Text(lyric.name1)
HStack {
Button(action: {
tap.isDoubleTab = false
}, label: {
Image(systemName: "suit.heart")
})
Button(action: {
tap.isDoubleTab = false
}, label: {
Image(systemName: "gear")
})
}
}
}
}
struct ContenView: View {
var body: some View {
ZStack {
if tap.isDoubleTab {
DetailMenu(dataArr: ListDataArray.dot[0])
} else {
AlphaHome() // This is just another struct
}
}
}
}
Related
In the last few months, many developers have reported NavigationLinks to unexpectedly pop out and some workarounds have been published, including adding another empty link and adding .navigationViewStyle(StackNavigationViewStyle()) to the navigation view.
Here, I would like to demonstrate another situation under which a NavigationLink unexpectedly pops out:
When there are two levels of child views, i.e. parentView > childLevel1 > childLevel2, and childLevel2 modifies childLevel1, then, after going back from level 2 to level 1, level 1 pops out and parentView is shown.
I have filed a bug report but not heard from apple since. None of the known workarounds seem to work. Does someone have an idea what to make of this? Just wait for iOS 15.1?
Below is my code (iPhone app). In the parent view, there is a list of persons from which orders are taken. In childLevel1, all orders from a particular person are shown. Each order can be modified by clicking on it, which leads to childLevel2. In childLevel2, several options are available (here only one is shown for the sake of brevity), which is the reason why the user is supposed to leave childLevel2 via "< Back".
import SwiftUI
struct Person: Identifiable, Hashable {
let id: Int
let name: String
var orders: [Order]
}
struct Pastry: Identifiable, Hashable {
let id: Int
let name: String
}
struct Order: Hashable {
var paId: Int
var n: Int // used only in the real code
}
class Data : ObservableObject {
init() {
pastries = [
Pastry(id: 0, name: "Prezel"),
Pastry(id: 1, name: "Donut"),
Pastry(id: 2, name: "bagel"),
Pastry(id: 3, name: "cheese cake"),
]
persons = [
Person(id: 0, name: "Alice", orders: [Order(paId: 1, n: 1)]),
Person(id: 1, name: "Bob", orders: [Order(paId: 2, n: 1), Order(paId: 3, n: 1)])
]
activePersonsIds = [0, 1]
}
#Published var activePersonsIds: [Int] = []
#Published var persons: [Person] = []
#Published var pastries: [Pastry]
#Published var latestOrder = Order(paId: 0, n: 1)
lazy var pastryName: (Int) -> String = { (paId: Int) -> String in
if self.pastries.first(where: { $0.id == paId }) == nil {
return "undefined pastryId " + String(paId)
}
return self.pastries.first(where: { $0.id == paId })!.name
}
var activePersons : [Person] {
return activePersonsIds.compactMap {id in persons.first(where: {$0.id == id})}
}
}
#main
struct Bretzel_ProApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
struct ContentView: View {
#Environment(\.colorScheme) var colorScheme
#StateObject var data = Data()
var body: some View {
TabView1(data: data)
// in the real code, there are more tabs
}
}
struct TabView1: View {
#StateObject var data: Data
var body: some View {
NavigationView {
List {
ForEach(data.activePersons, id: \.self) { person in
NavigationLink(
destination: EditPerson(data: data, psId: person.id),
label: {
VStack (alignment: .leading) {
Text(person.name)
}
}
)
}
}
.listStyle(PlainListStyle())
.navigationTitle("Orders")
}
.navigationViewStyle(StackNavigationViewStyle())
}
}
struct EditPerson: View {
#ObservedObject var data: Data
var psId: Int
var body: some View {
let pindex: Int = data.persons.firstIndex(where: { $0.id == psId })!
let p: Person = data.persons[pindex]
List() {
ForEach (0...p.orders.count-1, id: \.self) { loop in
Section(header:
HStack() {
Text("BESTELLUNG " + String(loop+1))
}
) {
EPSubview1(data: data, psId: psId, loop: loop)
}
}
}.navigationTitle(p.name)
.listStyle(InsetGroupedListStyle())
}
}
struct EPSubview1: View {
#ObservedObject var data: Data
var psId: Int
var loop: Int
var body: some View {
let pindex: Int = data.persons.firstIndex(where: { $0.id == psId })!
let p: Person = data.persons[pindex]
let o1: Order = p.orders[loop]
NavigationLink(
destination: SelectPastry(data: data)
.onAppear() {
data.latestOrder.paId = o1.paId
}
.onDisappear() {
data.persons[pindex].orders[loop].paId = data.latestOrder.paId
},
label: {
VStack(alignment: .leading) {
Text(String(o1.n) + " x " + data.pastryName(o1.paId))
}
}
)
}
}
struct SelectPastry: View {
#ObservedObject var data : Data
var body: some View {
VStack {
List {
ForEach(data.pastries, id: \.self) {pastry in
Button(action: {
data.latestOrder.paId = pastry.id
}) {
Text(pastry.name)
.foregroundColor(data.latestOrder.paId == pastry.id ? .primary : .secondary)
}
}
}.listStyle(PlainListStyle())
}
}
}
The problem is your ForEach. Despite that fact that Person conforms to Identifiable, you're using \.self to identify the data. Because of that, every time an aspect of the Person changes, so does the value of self.
Instead, just use this form, which uses the id vended by Identifiable:
ForEach(data.activePersons) { person in
Which is equivalent to:
ForEach(data.activePersons, id: \.id) { person in
I have a very weird issue with my double-tap gesture. I don't know this is a bug or my codes are wrong. Here, I want to use the Double Tap Gesture to open my DetailView by using NavigationLink. The problem is the page of my DetailView shows randomly which is supposed to match with the page of my SourceView. If I use a regular NavigationLink without using .onTapGesture, the seletected pages are matched. I couldn't find any solution online, but I found there are some bugs in NavigationLink. My codes examples are below. Please let me know if you have the same issue in using both onTapGesture and NavigationLink.
import SwiftUI
struct DataArray: Identifiable {
let id = UUID()
let number: Int
let cities: String
var name1: String?
var name2: String?
var name3: String?
var name4: String?
}
public struct ListDataArray {
static var dot = [
DataArray(number: 1,
cities: "Baltimore"
name1: "John",
name2: "Mike"),
DataArray(number: 2,
cities: "Frederick"),
DataArray(number: 3,
cities: "Catonsville"
name1: "Susan",
name2: "Oliver",
name3: "Jude",
name4: "Erik"),
]
}
struct Home: View {
var datas: [DataArray] = ListDataArray.dot
#State var isDoubleTab: Bool = false
var body: some View {
NavigationView {
LazyVStack(spacing: 10) {
ForEach (datas, id: \.id) { data in
HomeView(data: data)
.onTapGesture(count: 2) {
self.isDoubleTapped.toggle()
}
NavigationLink(
destination: DetailView(data: data),
isActivate: $isDoubleTab) {
EmptyView()
}
Divider()
.padding()
}
}
.navigationBarHidden(true)
}
}
}
struct DetailView: View {
var data = DataArray
var body: some View {
VStact {
Text(data.cities)
}
}
}
struct HomeView: View {
var data: DataArray
var body: some View {
VStack {
HSTack {
Text(data.cities).padding()
Text(data.name1)
if let sur2 = data.name2 {
surName2 = sur
Text(surName2)
}
}
}
}
}
The above examples are closely matched my current code. For simplicity, I just shortened some duplicate names including padding() etc...
The problem is that you're using the init(_:destination:isActive:) version of NavigationLink inside
a ForEach. When you set isDoubleTab to true, all of the visible NavigationLinks will attempt to present. This will result in the unexpected behavior that you're seeing.
Instead, you'll need to use a different version of NavigationLink: init(_:destination:tag:selection:). But first, replace #State var isDoubleTab: Bool = false with a property that stores a DataArray — something like #State var selectedData: DataArray?.
#State var selectedData: DataArray? /// here!
...
.onTapGesture(count: 2) {
/// set `selectedData` to the current loop iteration's `data`
/// this will trigger the `NavigationLink`.
selectedData = data
}
Then, replace your old NavigationLink(_:destination:isActive:) with this alternate version — instead of presenting once a Bool is set to true, it will present when tag matches selectedData.
NavigationLink(
destination: DetailView(data: data),
tag: data, /// will be presented when `tag` == `selectedData`
selection: $selectedData
) {
EmptyView()
}
Note that this also requires data, a DataArray, to conform to Hashable. That's as simple as adding it inside the struct definition.
struct DataArray: Identifiable, Hashable {
Result:
Full code:
struct Home: View {
var datas: [DataArray] = ListDataArray.dot
// #State var isDoubleTab: Bool = false // remove this
#State var selectedData: DataArray? /// replace with this
var body: some View {
NavigationView {
LazyVStack(spacing: 10) {
ForEach(datas, id: \.id) { data in
HomeView(data: data)
.onTapGesture(count: 2) {
selectedData = data
}
NavigationLink(
destination: DetailView(data: data),
tag: data, /// will be presented when `data` == `selectedData`
selection: $selectedData
) {
EmptyView()
}
Divider()
.padding()
}
}
.navigationBarHidden(true)
}
}
}
struct DetailView: View {
var data: DataArray
var body: some View {
VStack {
Text(data.cities)
}
}
}
struct HomeView: View {
var data: DataArray
var body: some View {
VStack {
HStack {
Text(data.cities).padding()
Text(data.name1 ?? "")
/// not sure what this is for, doesn't seem related to your problem though so I commented it out
// if let sur2 = data.name2 {
// surName2 = sur
// Text(surName2)
// }
}
}
}
}
public struct ListDataArray {
static var dot = [
DataArray(
number: 1,
cities: "Baltimore",
name1: "John",
name2: "Mike"
),
DataArray(
number: 2,
cities: "Frederick"
),
DataArray(
number: 3,
cities: "Catonsville",
name1: "Susan",
name2: "Oliver",
name3: "Jude",
name4: "Erik"
)
]
}
struct DataArray: Identifiable, Hashable {
let id = UUID()
let number: Int
let cities: String
var name1: String?
var name2: String?
var name3: String?
var name4: String?
}
Other notes:
Your original code has a couple syntax errors and doesn't compile.
You should rename DataArray to just Data, since it's a struct and not an array.
In a form, I'd like a user to be able to dynamically maintain a list of phone numbers, including adding/removing numbers as they wish.
I'm currently maintaining the list of numbers in a published array property of an ObservableObject class, such that when a new number is added to the array, the SwiftUI form will rebuild the list through its ForEach loop. (Each phone number is represented as a PhoneDetails struct, with properties for the number itself and the type of phone [work, cell, etc].)
Adding/removing works perfectly fine, but when I attempt to edit a phone number within a TextField, as soon as I type a character, the TextField loses focus.
My instinct is that, since the TextField is bound to the phoneNumber property of one of the array items, as soon as I modify it, the entire array within the class publishes the fact that it's been changed, hence SwiftUI dutifully rebuilds the ForEach loop, thus losing focus. This behavior is not ideal when trying to enter a new phone number!
I've also tried looping over an array of the PhoneDetails objects directly, without using an ObservedObject class as an in-between repository, and the same behavior persists.
Below is the minimum reproducible example code; as mentioned, adding/removing items works great, but attempting to type into any TextField immediately loses focus.
Can someone please help point me in the right direction as to what I'm doing wrong?
class PhoneDetailsStore: ObservableObject {
#Published var allPhones: [PhoneDetails]
init(phones: [PhoneDetails]) {
allPhones = phones
}
func addNewPhoneNumber() {
allPhones.append(PhoneDetails(phoneNumber: "", phoneType: "cell"))
}
func deletePhoneNumber(at index: Int) {
if allPhones.indices.contains(index) {
allPhones.remove(at: index)
}
}
}
struct PhoneDetails: Equatable, Hashable {
var phoneNumber: String
var phoneType: String
}
struct ContentView: View {
#ObservedObject var userPhonesManager: PhoneDetailsStore = PhoneDetailsStore(
phones: [
PhoneDetails(phoneNumber: "800–692–7753", phoneType: "cell"),
PhoneDetails(phoneNumber: "867-5309", phoneType: "home"),
PhoneDetails(phoneNumber: "1-900-649-2568", phoneType: "office")
]
)
var body: some View {
List {
ForEach(userPhonesManager.allPhones, id: \.self) { phoneDetails in
let index = userPhonesManager.allPhones.firstIndex(of: phoneDetails)!
HStack {
Button(action: { userPhonesManager.deletePhoneNumber(at: index) }) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $userPhonesManager.allPhones[index].phoneNumber)
}
}
Button(action: { userPhonesManager.addNewPhoneNumber() }) {
Label {
Text("Add Phone Number")
} icon: {
Image(systemName: "plus.circle.fill")
}
}.buttonStyle(BorderlessButtonStyle())
}
}
}
try this:
ForEach(userPhonesManager.allPhones.indices, id: \.self) { index in
HStack {
Button(action: {
userPhonesManager.deletePhoneNumber(at: index)
}) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $userPhonesManager.allPhones[index].phoneNumber)
}
}
EDIT-1:
Reviewing my comment and in light of renewed interest, here is a version without using indices.
It uses the ForEach with binding feature of SwiftUI 3 for ios 15+:
class PhoneDetailsStore: ObservableObject {
#Published var allPhones: [PhoneDetails]
init(phones: [PhoneDetails]) {
allPhones = phones
}
func addNewPhoneNumber() {
allPhones.append(PhoneDetails(phoneNumber: "", phoneType: "cell"))
}
// -- here --
func deletePhoneNumber(of phone: PhoneDetails) {
allPhones.removeAll(where: { $0.id == phone.id })
}
}
struct PhoneDetails: Identifiable, Equatable, Hashable {
let id = UUID() // <--- here
var phoneNumber: String
var phoneType: String
}
struct ContentView: View {
#ObservedObject var userPhonesManager: PhoneDetailsStore = PhoneDetailsStore(
phones: [
PhoneDetails(phoneNumber: "800–692–7753", phoneType: "cell"),
PhoneDetails(phoneNumber: "867-5309", phoneType: "home"),
PhoneDetails(phoneNumber: "1-900-649-2568", phoneType: "office")
]
)
var body: some View {
List {
ForEach($userPhonesManager.allPhones) { $phone in // <--- here
HStack {
Button(action: {
userPhonesManager.deletePhoneNumber(of: phone) // <--- here
}) {
Image(systemName: "minus.circle.fill")
}.buttonStyle(BorderlessButtonStyle())
TextField("Phone", text: $phone.phoneNumber) // <--- here
}
}
Button(action: { userPhonesManager.addNewPhoneNumber() }) {
Label {
Text("Add Phone Number")
} icon: {
Image(systemName: "plus.circle.fill")
}
}.buttonStyle(BorderlessButtonStyle())
}
}
}
I have a ForEach loop for my StudentStore and only want to display them in my list if the isFavorite field is true.
The problem is when I edit the value of isFavorite inside SecondView, it goes back to ContentView because of the if statement inside ContentView.
I want the list to update when I get back to ContentView rather than updating it immediately causing it to go back to the original page.
When I was looking for a solution I came upon this:
Read this entire thread thinking you were asking if you could make a NavigationView conditionally hidden and thought, “That sounds like a mess...”
Source: https://www.reddit.com/r/SwiftUI/comments/e2wn09/make_navigationlink_conditional_based_on_bool/
Can someone also how to solve this problem and why conditionally hiding a NavigationView is a bad idea?
Model
import SwiftUI
struct StudentItem: Identifiable {
var id = UUID()
var name: String
var isFavorite: Bool
init(name: String, isFavorite: Bool) {
self.name = name
self.isFavorite = isFavorite
}
}
class StudentStore: ObservableObject {
#Published var StudentStore: [StudentItem]
init(StudentStore: [StudentItem]){
self.StudentStore = StudentStore
}
}
Main view
struct ContentView: View {
#ObservedObject var studentStore = StudentStore(StudentStore: [StudentItem(name: "Stephen", isFavorite: true),
StudentItem(name: "Jay", isFavorite: true),
StudentItem(name: "Isaac", isFavorite: true),
StudentItem(name: "Talha", isFavorite: true),
StudentItem(name: "John", isFavorite: true),
StudentItem(name: "Matt", isFavorite: true),
StudentItem(name: "Leo", isFavorite: true)])
var body: some View {
NavigationView {
List {
ForEach(studentStore.StudentStore.indices, id: \.self) { index in
Group {
if self.studentStore.StudentStore[index].isFavorite == true {
NavigationLink(destination: SecondView(student: self.$studentStore.StudentStore[index])) {
HStack {
Text(self.studentStore.StudentStore[index].name)
Image(systemName: self.studentStore.StudentStore[index].isFavorite ? "star.fill" : "star")
}
}
}
}
}
}
}
}
}
Sub view
struct SecondView: View {
#Binding var student: StudentItem
var body: some View {
Button(action: {
self.student.isFavorite.toggle()
}) {
Image(systemName: student.isFavorite ? "star.fill" : "star")
}
}
}
The problem makes sense, it's a Binded value. The Navigation to that person SecondView technically no longer exists according to the Binding.
You could create a localized State for SecondView, set your Image and Button to work that that property, and then use onDisappear to update the StudentItem as you Navigate back.
struct SecondView: View {
#Binding var student: StudentItem
#State var isFavorite: Bool = true
var body: some View {
Button(action: {
self.isFavorite.toggle()
}) {
Image(systemName: self.isFavorite ? "star.fill" : "star")
}.onDisappear {
self.student.isFavorite = self.isFavorite
}
}
}
The above will work with the rest of your code.
As for not using an if statement, I kind of get it. For me, I would want to iterate through the values I know I want to include. Not saying this is perfect, but you could filter the list before you go in, and then summon the Bindable reference to the StudentItem to pass into SecondView. Be sure to make StudentItem Hashable for this to work:
var body: some View {
NavigationView {
List {
ForEach(studentStore.StudentStore.filter({$0.isFavorite == true}), id: \.self) { student in
Group {
NavigationLink(destination: SecondView(student: self.$studentStore.StudentStore[self.studentStore.StudentStore.firstIndex(of: student)!])) {
HStack {
Text(student.name)
Image(systemName: student.isFavorite ? "star.fill" : "star")
}
}
}
}
}
}
}
A simple way to express this would be to go back to UITableView and have a didSelectRowAt(indexPath) function that behaved like this:
if (indexPath.row == 0) { ... } else { ... }
Where based upon the indexPath.row value, I can call a unique view controller (ex: the first one is a TableView and the others are CollectionViews.
Currently, based upon the two answers thus far, I can produce the following code:
import SwiftUI
struct MenuItem {
let title: String
let isEnabled: Bool
}
struct HomeList: View {
let menuItems = [
MenuItem(title: "ABC", isEnabled: true),
MenuItem(title: "DEF", isEnabled: false),
MenuItem(title: "GHI", isEnabled: true)
]
var body: some View {
NavigationView {
List {
ForEach(menuItems.indices, id: \.self) { index in
NavigationLink(destination: menuItems[index].title == "ABC" ?
FirstList() :
SecondView(menuItem: menuItems[index])) {
HomeRow(menuItem: menuItems[index])
}
}
}
}
}
}
struct HomeRow: View {
var menuItem: MenuItem
var body: some View {
HStack {
Text(verbatim: menuItem.title)
}
}
}
struct FirstList: View {
var body: some View {
List(1 ..< 5) { index in
Text("Row \(index)")
}
.listStyle(GroupedListStyle())
}
}
struct SecondView: View {
var menuItem: MenuItem
var body: some View {
Text(menuItem.title)
}
}
However, I get the following error with my NavigationLink:
Result values in '? :' expression have mismatching types 'FirstList'
and 'SecondView'
Since my goal here is to have two different views I point to based upon the title, I'd like to find some way to make that work.
The answer posted by superpuccio seems to be pretty close to what I want, but with the expected complexity of the target views, I do not think it would be feasible to compose them entirely within NavigationLink.
Since you have a dynamic List I suggest you use a ForEach inside a List this way:
import SwiftUI
struct MenuItem {
let title: String
let isEnabled: Bool
}
struct HomeList: View {
let menuItems = [
MenuItem(title: "ABC", isEnabled: true),
MenuItem(title: "DEF", isEnabled: false),
MenuItem(title: "GHI", isEnabled: true)
]
var body: some View {
let firstRowModel = menuItems[0]
let actualModel = menuItems[1...menuItems.count-1]
return NavigationView {
List {
NavigationLink(destination: FirstList()) {
HomeRow(menuItem: firstRowModel)
}
ForEach(actualModel.indices, id: \.self) { index in
NavigationLink(destination: SecondView(menuItem: actualModel[index])) {
HomeRow(menuItem: actualModel[index])
}
}
}
}
}
}
struct HomeRow: View {
var menuItem: MenuItem
var body: some View {
HStack {
Text(verbatim: menuItem.title)
}
}
}
struct FirstList: View {
var body: some View {
List(1 ..< 5) { index in
Text("Row \(index)")
}
.listStyle(GroupedListStyle())
}
}
struct SecondView: View {
var menuItem: MenuItem
var body: some View {
Text(menuItem.title)
}
}
I would include the condition in the destination.
var body: some View {
NavigationView {
List(1 ..< 5) { idx in
NavigationLink(destination:
idx < 3 ? Text("1111") : Text("2222") ) {
Text("Row \(idx)")
}
}
.listStyle(GroupedListStyle())
}
}