I'm trying to make two List
components: one of them is static and small, the second is incredibly large and dynamic. In the first List
I store food categories: Alcoholic products, Soups, Cereals, etc. In the second List
, the word is searched directly from the database - it can be anything: a dish or a category of dishes. Below is the code - it displays the start page. Initially, the first static and small List
is located on it, as well as the Search component (Navigationview.seacrhable()
). When you type a word into the search bar, the first List
disappears and the second one appears. At the moment, both sheets are loaded asynchronously. This is necessary because the second sheet is really big (thousands of rows). This is where my problem begins. Sometimes, when you type a word into the search bar, a copy of this sheet appears on top of it, as shown in the image. It only happens for a fraction of a second, but it's still noticeable. The problem is most likely due to asynchronous loading, before I added it, the List
was loading super slowly, but without such bugs.
My minimal reproducible example:
ContentView.sfiwt
Main List, displaying the food categories available for selection.
import SwiftUI
struct ContentView: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@State public var addScreen: Bool = true
@State private var searchByWordView: Bool = true
@State private var searchByWordCategoryView: Bool = true
@State public var gram: String = ""
@State private var selectedFood: String = ""
@State private var selectedFoodCategoryItem: String = ""
@State private var selectedFoodTemp: String = ""
@State private var selectedFoodCategoryTemp: String = ""
@State private var FoodCList: [FoodCategory] = []
@State private var FoodList: [FoodItemByName] = []
@State var foodItems: [String] = []
@MainActor
var body: some View {
NavigationView {
ZStack {
List {
if !searchByWordView {
Section {
ForEach(FoodList, id:\.self){i in
Button(action: {
selectedFoodTemp = i.name
addScreen.toggle()
}){Text("\(i.name)")}.foregroundColor(.black)
}
}
} else {
Section {
ForEach(FoodCList, id:\.self){i in
NavigationLink(destination: GetFoodCategoryItemsView(category: "\(i.name)")) {
Text("\(i.name)")
}.foregroundColor(.black)
}
}
}
}
.listStyle(.plain)
.task{
FoodCList = await FillFoodCategoryList()
}
if !addScreen {
addSreenView(addScreen: $addScreen, gram: $gram, selectedFood: $selectedFoodTemp, foodItems: $foodItems)
}
}
.navigationTitle("Add the dish")
.navigationBarTitleDisplayMode(.inline)
}
.searchable(
text: $selectedFood,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search by word"
)
.onChange(of: selectedFood, perform: {i in
if i.isEmpty {
searchByWordView = true
} else {
searchByWordView = false
Task {
FoodList = await GetFoodItemsByName(_name: selectedFood)
}
}
})
}
func GetFoodCategoryItemsView(category: String) -> some View {
ZStack {
List {
if !searchByWordCategoryView {
Section {
ForEach(GetFoodCategoryItems(_category: category).filter{$0.name.contains(selectedFoodCategoryItem)}, id:\.self){i in
Button(action: {
selectedFoodCategoryTemp = i.name
addScreen.toggle()
}){Text("\(i.name)")}
}
}
} else {
Section {
ForEach(GetFoodCategoryItems(_category: category), id:\.self){i in
Button(action: {
selectedFoodCategoryTemp = i.name
addScreen.toggle()
}){Text("\(i.name)")}
}
}
}
}
if !addScreen {
addSreenView(addScreen: $addScreen, gram: $gram, selectedFood: $selectedFoodCategoryTemp, foodItems: $foodItems)
}
}
.searchable(
text: $selectedFoodCategoryItem,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search by word"
)
.onChange(of: selectedFoodCategoryItem, perform: {i in
if i.isEmpty {
searchByWordCategoryView = true
} else {
searchByWordCategoryView = false
}
})
.listStyle(.plain)
.navigationTitle(category)
.navigationBarTitleDisplayMode(.inline)
.interactiveDismissDisabled()
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
AddSreenView.swift
Modal window of entering grams of food consumed.
import SwiftUI
struct addSreenView: View {
@Binding var addScreen: Bool
@Binding var gram: String
@Binding var selectedFood: String
@Binding var foodItems: [String]
var body: some View {
ZStack{
Color(.black)
.opacity(0.3)
.ignoresSafeArea()
.onTapGesture{withAnimation(.linear){addScreen.toggle()}}
VStack(spacing:0){
Text("Add a dish/meal")
.padding()
Divider()
VStack(){
TextField("Weight, in gram", text: $gram)
.padding(.leading, 16)
.padding(.trailing, 16)
Rectangle()
.frame(height: 1)
.foregroundColor(.black)
.padding(.leading, 16)
.padding(.trailing, 16)
}.padding()
Divider()
HStack(){
Button(action: {
foodItems.append("\(selectedFood), \(gram) g.")
gram = ""
selectedFood = ""
addScreen.toggle()
}){
Text("Save")
.frame(maxWidth: .infinity)
.foregroundColor(.black)
}
.frame(maxWidth: .infinity)
Divider()
Button(action: {
gram = ""
selectedFood = ""
addScreen.toggle()
}){
Text("Cancel")
.frame(maxWidth: .infinity)
.foregroundColor(.black)
}
.frame(maxWidth: .infinity)
}.frame(height: 50)
}
.background(Color.white.cornerRadius(10))
.frame(maxWidth: 350)
}
}
}
GetFunction.swift
This file simulates my real SQLite database queries.
import Foundation
import SwiftUI
// Fill start pade List (all categories of dishes)
struct FoodCategory: Identifiable, Hashable {
let name: String
let id = UUID()
}
func FillFoodCategoryList() async -> [FoodCategory] {
let catList: [FoodCategory] = [FoodCategory(name: "Alcohol"),FoodCategory(name: "Soups"),FoodCategory(name: "Cereals"),FoodCategory(name: "Fish"),FoodCategory(name: "Meat")]
return catList
}
// Search by word List
struct FoodItemByName: Identifiable, Hashable {
let name: String
let id = UUID()
}
func GetFoodItemsByName(_name: String) async -> [FoodItemByName] {
var foodItemsByName: [FoodItemByName] = []
let items: [String] = ["Light beer with 11% dry matter in the original wort","Light beer with 20% of dry matter in the original wort","Dark beer with 13% dry matter content in the original wort","Dark beer, with a proportion of dry substances in the initial wort of 20%","Dry white and red wines (including champagne)","Semi-dry white and red wines (including champagne)","Semi-sweet white and red wines (including champagne)","Sweet white and red wines (including champagne)","Strong wines","Semi-dessert wines","Dessert wines","Liqueur wines","Slivyanka liqueur","Cherry liqueur","Ordinary cognac - Three stars","Vodka"]
let filteredItems = items.filter{ $0.contains("\(_name)") }
for i in filteredItems {
foodItemsByName.append(FoodItemByName(name: i))
}
return foodItemsByName
}
// List appears when you click on Alcohole in start page (by analogy with other categories of dishes)
struct FoodCategoryItem: Identifiable, Hashable {
let name: String
let id = UUID()
}
func GetFoodCategoryItems(_category: String) -> [FoodCategoryItem] {
var foodCategoryItems: [FoodCategoryItem] = []
let _alcohole: [String] = ["Light beer with 11% dry matter in the original wort","Light beer with 20% of dry matter in the original wort","Dark beer with 13% dry matter content in the original wort","Dark beer, with a proportion of dry substances in the initial wort of 20%","Dry white and red wines (including champagne)","Semi-dry white and red wines (including champagne)","Semi-sweet white and red wines (including champagne)","Sweet white and red wines (including champagne)","Strong wines","Semi-dessert wines","Dessert wines","Liqueur wines","Slivyanka liqueur","Cherry liqueur","Ordinary cognac - Three stars","Vodka"]
let _soup: [String] = ["Chicken soup","French onion soup","Tomato soup","Chicken Dumpling Soup","Beef Stew","Cream of Potato","Lobster Bisque","Chili Con Carne","Clam Chowder","Cream Of Cheddar Broccoli"]
let _cereals: [String] = ["Cinnamon Toast Crunch","Frosted Flakes","Honey Nut Cheerios","Lucky Charms","Froot Loops","Fruity Pebbles","Cap'n Crunch","Cap'n Crunch's Crunch Berries","Cocoa Puffs","Reese's Puffs"]
let _fish: [String] = ["Salmon","Tuna","Cod","Rainbow Trout","Halibut","Red Snapper","Flounder","Bass","Mahi-Mahi","Catfish"]
let _meat: [String] = ["Beef","Chicken (Food)","Lamb","Pork","Duck","Turkey","Venison","Buffalo","American Bison"]
if _category == "Alcohol"{
for i in _alcohole{foodCategoryItems.append(FoodCategoryItem(name: i))}
} else if _category == "Soups" {
for i in _soup{foodCategoryItems.append(FoodCategoryItem(name: i))}
} else if _category == "Cereals" {
for i in _cereals{foodCategoryItems.append(FoodCategoryItem(name: i))}
} else if _category == "Fish" {
for i in _fish{foodCategoryItems.append(FoodCategoryItem(name: i))}
} else {
for i in _meat{foodCategoryItems.append(FoodCategoryItem(name: i))}
}
return foodCategoryItems
}
CodePudding user response:
Besides using id
for the IDs, as mentioned in the comments, you can do some refactoring to get SwiftUI to not re-render as much of the view hierarchy and instead reuse components. For example, you have an if
condition and in each you have separate Section
, ForEach
, etc components. Instead, you could render the content of the ForEach
based on the state of the search:
func categoryItems(category: String) -> [FoodCategoryItem] {
if !searchByWordCategoryView {
return GetFoodCategoryItems(_category: category).filter{$0.name.contains(selectedFoodCategoryItem)}
} else {
return GetFoodCategoryItems(_category: category)
}
}
func GetFoodCategoryItemsView(category: String) -> some View {
ZStack {
List {
Section {
ForEach(categoryItems(category: category)){i in
Button(action: {
selectedFoodCategoryTemp = i.name
addScreen.toggle()
}){Text("\(i.name)")}
}
}
if !addScreen {
addSreenView(addScreen: $addScreen, gram: $gram, selectedFood: $selectedFoodCategoryTemp, foodItems: $foodItems)
}
}
}
.searchable(
text: $selectedFoodCategoryItem,
placement: .navigationBarDrawer(displayMode: .always),
prompt: "Search by word"
)
.onChange(of: selectedFoodCategoryItem, perform: {i in
if i.isEmpty {
searchByWordCategoryView = true
} else {
searchByWordCategoryView = false
}
})
.listStyle(.plain)
.navigationTitle(category)
.navigationBarTitleDisplayMode(.inline)
.interactiveDismissDisabled()
}