OK, I nailed down the problem to the dynamic list with sections, so I changed the title and I'm going to shorten my post...
In my project, there is a complex view that contains a dynamic list with sections that depend on many state variables.
Problem: after a few user interactions and subsequent changes to the list, I see strange behaviour:
sometimes a section doesn't show up although the condition of the if-clause in which it is embedded is definitely met
sometimes, the section header label is replaced with the label of a list item
sometimes, it crashes
when the view is broken and I enforce a refresh of the view without changing any of the state variables, everything looks good again
The code below is taken from my project, however, greatly simplified by removing parts that are not necessary to produce the error. The state variable altered by user interaction is pIdStatus. From pIdStatus, the function data.analyze() calculates the arrays pastriesWithStatus1, pastriesWithStatus2 and pastriesWithStatus3.
Interestingly, when I remove the tab-view in which everything is embedded, everything works fine.
Any help is greatly appreciated.
import SwiftUI
struct ContentView: View {
#StateObject var data = Data()
var body: some View {
TabView { // needed in my project and needed for crash.
List {
// for debugging purpose: create a button that triggers a refresh
Button {
data.counter += 1
} label: {
HStack() {
Text("Refresh")
.font(.system(size: 16, weight: .regular))
}
}
// Section with status3-items
if data.pastriesWithStatus3.count > 0 {
Section(header:
HStack() {
Text("STATUS 3")
}
){
ForEach(data.pastriesWithStatus3, id: \.self) { i in
subview21(data: data, label: data.pastries.first(where: { $0.id == i })!.name, id: i, status: 3)
}
}
}
// section with other items
Section(header:
HStack() {
Text("OTHER ITEMS")
}
){
ForEach(data.pastriesWithStatus2, id: \.self) { i in
subview21(data: data, label: data.pastries.first(where: { $0.id == i })!.name, id: i, status: 2)
}
ForEach(data.pastriesWithStatus1, id: \.self) { i in
subview21(data: data, label: data.pastries.first(where: { $0.id == i })!.name, id: i, status: 1)
}
}
}
.listStyle(InsetGroupedListStyle())
.tabItem {
Image(systemName: "cart")
Text("shopping")
}.onAppear {
data.analyze()
}
}
}
}
struct subview21: View {
#ObservedObject var data : Data
let hspacing: CGFloat = 20
let tab1: CGFloat = 150
var label: String
var id: Int
var status: Int // 1=red, 2=unchecked, 3=green
var body: some View {
if data.testcondition { // this line is required for crash. Although it is always true.
HStack (spacing: hspacing) {
Text(data.pastries.first(where: { $0.id == id })!.name)
.frame(width: tab1, alignment: .leading)
Button {
data.pIdStatus[id] = 1
data.analyze()
} label: {
Image(systemName: (status == 1 ? "checkmark.square.fill" : "square"))
.foregroundColor(.red)
}
.buttonStyle(BorderlessButtonStyle())
Button {
data.pIdStatus[id] = 3
data.analyze()
} label: {
Image(systemName: (status == 3 ? "checkmark.square.fill" : "square"))
.foregroundColor(.green)
}
.buttonStyle(BorderlessButtonStyle())
}
}
}
}
let maxPId = 99
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 pastryId: Int
var n: Int
var otherwise: OrderL2?
}
struct OrderL2: Hashable {
let level: Int = 2
var pastryId: Int
var n: Int
}
class Data : ObservableObject {
#Published var persons: [Person] = [
Person(id: 1, name: "John", orders: [Order(pastryId: 0, n:1, otherwise:OrderL2(pastryId: 1, n:2))]),
]
#Published var pastries: [Pastry] = [Pastry(id: 0, name: "Donut"),
Pastry(id: 1, name: "prezel")]
#Published var counter: Int = 0 // for debugging
#Published var pIdStatus: [Int] = Array(repeating: 2, count: maxPId)
#Published var testcondition: Bool = true // needed in my project, for simplicity, here, always true
#Published var pastriesWithStatus1: [Int] = []
#Published var pastriesWithStatus2: [Int] = []
#Published var pastriesWithStatus3: [Int] = []
func analyze() {
var localPastriesWithStatus1: [Int] = []
var localPastriesWithStatus2: [Int] = []
var localPastriesWithStatus3: [Int] = []
for p in pastries { // there may be more elegant ways to code, however, in real life, there's a lot more functionality here
if p.id < maxPId {
if pIdStatus[p.id] == 1 {
localPastriesWithStatus1.append(p.id)
}
if pIdStatus[p.id] == 2 {
localPastriesWithStatus2.append(p.id)
}
if pIdStatus[p.id] == 3 {
localPastriesWithStatus3.append(p.id)
}
}
}
pastriesWithStatus1 = localPastriesWithStatus1
pastriesWithStatus2 = localPastriesWithStatus2
pastriesWithStatus3 = localPastriesWithStatus3
}
}
Apparently, swiftui List is not made for Lists that are built from scratch each time the view is refreshed. Instead, Lists must be linked to arrays to/from which items are added/removed one by one.
I solved the problem by avoiding List altogether, instead using Stack, Text etc.
Related
I have a List, with custom Stepper inside each row. Therefore, when I scroll my stepper is reset. (The increment and decrement works when is visible. When it disappear, it's reset. Don't keep the state. It's alway's reset).
Xcode: v14.2 / Simulator iOS: 16.2
struct Product: Codable, Hashable, Identifiable {
let id: String
let name: String
let step: Int
let quantity: Int
let priceHT: Double
}
class ProductViewModel: ObservableObject {
#Published var products = [Product]()
...
}
struct ProductListView: View {
#EnvironmentObject var productViewModel: ProductViewModel
var body: some View {
List(productViewModel.products) { product in
ProductRowView(product: product)
}
}
}
My List row:
I tried to modify #State with #binding, but without success.
struct ProductRowView: View {
#State var product: Product
var body: some View {
HStack {
VStack {
Text(product.name)
Text(String(format: "%.2f", product.priceHT) + "€ HT")
}
Spacer()
MyStepper(product: $product, value: product.quantity)
.font(.title)
}
}
}
My Custom stepper:
struct MyStepper: View {
#Binding var product: Product
#State var value: Int = 0
var body: some View {
HStack {
VStack() {
HStack {
Button(action: {
value -= product.step
if let row = orderViewModel.productsOrder.firstIndex(where: { $0.name == product.name }) {
let order = Product(id: product.id, name: product.name, step: product.step, quantity: value, priceHT: product.priceHT)
if (value == 0) {
orderViewModel.productsOrder.remove(at: row)
} else {
orderViewModel.productsOrder[row] = order
}
}
}, label: {
Image(systemName: "minus.square.fill")
})
Text(value.formatted())
Button(action: {
value += product.step
let order = Product(id: product.id, name: product.name, step: product.step, quantity: value, priceHT: product.priceHT)
if let row = orderViewModel.productsOrder.firstIndex(where: { $0.name == product.name }) {
orderViewModel.productsOrder[row] = order
} else {
orderViewModel.productsOrder.append(order)
}
}, label: {
Image(systemName: "plus.app.fill")
})
}
Text(product.unit)
}
}
}
}
Thks
EDIT / RESOLVED
Here is the solution for my case :
Change type of quantity. let to var
struct Product: Codable, Hashable, Identifiable {
...
var quantity: Int
...
}
Delete #State in MyStepper and replace value by product.quantity
Use bindings for that, e.g.
struct ProductListView: View {
#EnvironmentObject var model: Model
var body: some View {
List($model.products) { $product in
ProductRowView(product: $product)
}
}
}
struct ProductRowView: View {
#Binding var product: Product // now you have write access to the Product struct
...
However, to make the View reusable and to help with previewing, it's best to pass in only the simple types the View needs, e.g.
struct TitlePriceView: View {
let title: String
#Binding var price: Double
// etc.
TitlePriceView(title: product.title, price: $product.price)
I am attempting to append data to a SwiftUI list section. When I do this in the example code below, the console displays an warning which it forebodingly indicates, "will become an assert in the future."
struct ContentView: View {
struct Rows: Hashable {
let id: String
let rows: [String]
}
#State var sections = [
Rows(id: UUID().uuidString, rows: [])
]
var body: some View {
VStack {
Button("Add Row") {
let lastRows = sections.last!
let rows = Rows(id: lastRows.id, rows: lastRows.rows + [UUID().uuidString])
self.sections = [rows]
}
List {
ForEach(sections, id: \.self) { exercise in
Section(exercise.id) {
ForEach(exercise.rows, id: \.self) { row in
Text(row)
}
}
}
}
}
}
}
What exactly am I doing wrong?
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 created the list showing the array of digits. When you select multiple digits and hit the button below, it should tell you the sum of the selected digits.
However, when you select the same digits in the different rows, both of them get checked at a time. I understand that this should be fixed by using UUID, but the final result that I want is the sum of the digits, which is Int. Therefore, I have been lost... Can someone tell me how to make it right, please?
Also, the last row doesn't get checked even when selected for some reason, which is so weird.
Here is the link to the gif showing the current situation and the entire code below.
import SwiftUI
struct MultipleSelectionList: View {
#State var items: [Int] = [20, 20, 50, 23, 3442, 332]
#State var selections: [Int] = []
#State var result: Int = 0
var body: some View {
VStack {
List {
ForEach(items, id: \.self) { item in
MultipleSelectionRow(value: item, isSelected: self.selections.contains(item)) {
if self.selections.contains(item) {
self.selections.removeAll(where: { $0 == item })
}
else {
self.selections.append(item)
}
}
}
}
Button(action: {
result = selections.reduce(0, +)
}, label: {
Text("Show result")
})
.padding()
Text("Result is \(result)")
Text("The number of items in the array is \(selections.count)")
.padding()
Spacer()
}
}
}
struct MultipleSelectionRow: View {
var value: Int
var isSelected: Bool
var action: () -> Void
var body: some View {
Button(action: self.action) {
HStack {
Text("\(self.value)")
if self.isSelected {
Spacer()
Image(systemName: "checkmark")
}
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
MultipleSelectionList()
}
}
You could just store the index of the items in selections, instead of the actual items.
You could do something like:
ForEach(items.indices) { i in
MultipleSelectionRow(value: items[i], isSelected: self.selections.contains(i)) {
if self.selections.contains(i) {
self.selections.removeAll(where: { $0 == i })
}
else {
self.selections.append(i)
}
}
}
I have not run this so there may be errors but you get the idea.
Before you look at the code below try making your items: [Int] into a items: [UUID: Int] this would mimic having an Identifiable object.
Programming is about figuring how to make things happen.
import SwiftUI
struct MultipleSelectionList: View {
#State var items: [UUID:Int] = [UUID():20, UUID():20, UUID():50, UUID():23, UUID():3442, UUID():332]
#State var selections: [UUID:Int] = [:]
#State var result: Int = 0
var body: some View {
VStack {
List {
ForEach(items.sorted(by: { $0.1 < $1.1 }), id: \.key) { key, value in
MultipleSelectionRow(value: value, isSelected: self.selections[key] != nil) {
if self.selections[key] != nil {
self.selections.removeValue(forKey: key)
}
else {
self.selections[key] = value
}
}
}
}
Button(action: {
result = selections.values.reduce(0, +)
}, label: {
Text("Show result")
})
.padding()
Text("Result is \(result)")
Text("The number of items in the array is \(selections.count)")
.padding()
Spacer()
}
}
}
struct MultipleSelectionRow: View {
var value: Int
var isSelected: Bool
var action: () -> Void
var body: some View {
Button(action: self.action) {
HStack {
Text("\(self.value)")
if self.isSelected {
Spacer()
Image(systemName: "checkmark")
}
}
}
}
}
struct MultipleSelect_Previews: PreviewProvider {
static var previews: some View {
MultipleSelectionList()
}
}
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())
}
}