Home > Back-end >  Bug in animation when loading List asynchronously
Bug in animation when loading List asynchronously

Time:11-24

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
}

enter image description here

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()
}
  • Related