Double Tap Gesture with NavigationLink produce random stack of DetailView - swiftui

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.

Related

SwiftUI - Binding var updates the original value only the first time, and then it doesn't update anymore

I have a Picker that updates a #Binding value, that is linked to the original #State value. The problem is that it gets updated only the first time I change it, and then it always remains like that. Not only in the sheet, but also in the ContentView, which is the original view in which the #State variable is declared. Here's a video showing the problem, and here's the code:
ContentView:
struct ContentView: View {
#State var cityForTheView = lisbon
#State var showCitySelection = false
var body: some View {
VStack {
//View
}
.sheet(isPresented: $showCitySelection) {
MapView(cityFromCV: $cityForTheView)
}
}
}
MapView:
struct MapView: View {
#Environment(\.presentationMode) var presentationMode
#Binding var cityFromCV : CityModel
#State var availableCities = [String]()
#State var selectedCity = "Lisbon"
var body: some View {
NavigationView {
ZStack {
VStack {
Form {
HStack {
Text("Select a city:")
Spacer()
Picker("", selection: $selectedCity) {
ForEach(availableCities, id: \.self) {
Text($0)
}
}
.accentColor(.purple)
}
.pickerStyle(MenuPickerStyle())
Section {
Text("Current city: \(cityFromCV.cityName)")
}
}
}
}
}
.interactiveDismissDisabled()
.onAppear {
availableCities = []
for ct in cities {
availableCities.append(ct.cityName)
}
}
.onChange(of: selectedCity) { newv in
self.cityFromCV = cities.first(where: { $0.cityName == newv })!
}
}
}
CityModel:
class CityModel : Identifiable, Equatable, ObservableObject, Comparable {
var id = UUID()
var cityName : String
var country : String
var imageName : String
init(cityName: String, country: String, imageName : String) {
self.cityName = cityName
self.country = country
self.imageName = imageName
}
static func == (lhs: CityModel, rhs: CityModel) -> Bool {
true
}
static func < (lhs: CityModel, rhs: CityModel) -> Bool {
true
}
}
var cities = [
lisbon,
CityModel(cityName: "Madrid", country: "Spain", imageName: "Madrid"),
CityModel(cityName: "Barcelona", country: "Spain", imageName: "Barcelona"),
CityModel(cityName: "Paris", country: "France", imageName: "Paris")
]
var lisbon = CityModel(cityName: "Lisbon", country: "Portugal", imageName: "Lisbon")
What am I doing wrong?
The problem are:
at line 35: you use selectedCity to store the selected data from your picker.
Picker("", selection: $selectedCity) {
then at your line 46: you use cityFromCV.cityName to display the data which will always show the wrong data because you store your selected data in the variable selectedCity.
Section {
Text("Current city: \(cityFromCV.cityName)")
}
Solution: Just change from cityFromCV.cityName to selectedCity.

SwiftUI #State wierd wrappedValue behaviour

So straight to the point i have this code:
struct DataModel {
var option: String
}
struct ViewModel {
var selected: String? = "1"
var options = [DataModel(option: "1"), DataModel(option: "2"), DataModel(option: "3")]
}
struct ContentView: View {
#State var viewModel: ViewModel
var body: some View {
VStack {
Picker("test", selection: aBinder()) {
ForEach(viewModel.options, id: \.option) { option in
Text(option.option)
}
}
.background(Color.red)
}
}
func aBinder() -> Binding<String?> {
Binding<String?> {
viewModel.selected
} set: { value in
$viewModel.selected.wrappedValue = value
print($viewModel.selected.wrappedValue)
}
}
}
The value of "selected" in the viewModel doesn't change.
This works:
struct DataModel {
var option: String
}
struct ViewModel {
var selected: String = "1"
var options = [DataModel(option: "1"), DataModel(option: "2"), DataModel(option: "3")]
}
struct ContentView: View {
#State var viewModel: ViewModel
var body: some View {
VStack {
Picker("test", selection: $viewModel.selected) {
ForEach(viewModel.options, id: \.option) { option in
Text(option.option)
}
}
Button("press me", action: { print(viewModel.selected) })
}
}
}
But that doesn't make any sense. in both cases i use a binding to store the current value. What is going on? I'm pretty new to swiftUI so i might have missed how something works.
Thanks in advance
The types don’t match selected is a String and the options are a DataModel. The types have to match exactly.

iOS 15: Navigation link popping out, again

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

Double tap gesture fail to update

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

Multiple NavigationLink inside a List using SwiftUI

I am trying to have two NavigationLinks inside a struct that I use as a NavigationLink inside a List with ForEach but the code does not work (the navigation does not happen to either View) , here is the code:
struct mainView : View {
#State var people = [Person(name: "one", family: "1", type: "men"),Person(name: "two", family: "2", type: "women")]
var body : some View {
List{
ForEach(self.people, id:\.self){person in
NavigationLink(destination: Text("hi")) {
PersonView(person : person)
}.buttonStyle(BorderlessButtonStyle())
}
}
}
struct Person {
var name : String
var family : String
var type : String
}
struct PersonView : View {
#State var person : Person?
var body : some View {
HStack{
NavigationLink(destination: Text(self.person.name)) {
Text("this is \(self.person.name) \(self.person.family)")
}
NavigationLink(destination: Text(self.person.type)) {
Text(self.person.type)
}
}
}
}
I have added the .buttonStyle after reading it might help but it did nothing. I also tried the following PersonView :
struct PersonView : View {
#State var person : Person?
#State var goToFirst = false
#State var goToType = false
var body : some View {
VStack{
Button(action:{
self.goToFirst.toggle()
}){
Text("this is \(self.person.name) \(self.person.family)")
}.buttonStyle(BorderlessButtonStyle())
Button(action:{
self.goToType.toggle()
}){
Text(self.person.type)
}.buttonStyle(BorderlessButtonStyle())
NavigationLink(destination: Text(self.person.name), isActive : self.$goToFirst) {
Text("").frame(width: 0.01, height: 0.01)
}
NavigationLink(destination: Text(self.person.type), isActive : self.$goToType) {
Text("").frame(width: 0.01, height: 0.01)
}
}
}
This did not work as well, what am I supposed to do so when I click anywhere on the cell it will navigate to Text("hi") and when I click on the respective areas it will navigate to the proper View .
Important: This view is getting navigated to from a view that is wrapped with a NavigationView
You have not added a navigationView, that is the problem.
struct MainView: View {
#State private var peoples = [Person(name: "one", family: "1", type: "men"),Person(name: "two", family: "2", type: "women")]
var body: some View {
NavigationView { //This is important
List {
ForEach(self.peoples, id: \.name) { people in
NavigationLink(destination: PersonView(person: people)) {
Text(people.name)
}
}
}
.navigationTitle("Demo")
}
}
}
struct Person {
var name : String
var family : String
var type : String
}
struct PersonView : View {
#State var person : Person?
var body : some View {
HStack{
NavigationLink(destination: Text(self.person.name)) {
Text("this is \(self.person.name) \(self.person.family)")
}
NavigationLink(destination: Text(self.person.type)) {
Text(self.person.type)
}
}
}
}
I think the following does what you want. The code can be pasted into an empty project.
Some clarifications:
The List seems to make things more difficult, so I replaced it with a VStack. You may need to surround it with a ScrollView.
Also, like user Asperi hinted, this works best when you create multiple NavigationLink but around an EmptyView that is then later activated.
struct MainView: View {
#State var people = [Person(name: "one", family: "1", type: "men"), Person(name: "two", family: "2", type: "women")]
var body: some View {
VStack {
ForEach(self.people, id: \.self) { person in
PersonView(person: person)
}
}
}
}
struct Person: Hashable {
var name: String
var family: String
var type: String
}
struct PersonView: View {
let person: Person
#State private var selection: Int?
var body: some View {
HStack {
NavigationLink(
destination: Text(self.person.name),
tag: self.person.hashValue + 1,
selection: self.$selection) {
EmptyView()
}
NavigationLink(
destination: Text(self.person.type),
tag: self.person.hashValue + 2,
selection: self.$selection) {
EmptyView()
}
Button("this is \(self.person.name) \(self.person.family)") {
self.selection = self.person.hashValue + 1
}
Spacer()
Button(self.person.type) {
self.selection = self.person.hashValue + 2
}.padding()
}
}
}
struct ContentView: View {
var body: some View {
NavigationView {
MainView()
}
}
}