Bug in animation when loading List asynchronously - swift
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
}
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
How to display saved data in TextEditor when view is reopen
I am building the app that will store vertical and horizontal measurements at 12 doors. In there, for each loop creates 12 Sections with vertical and horizontal readings TextEditors for each door. Each text editor stores values to one of the arrays. When done entering I used UserDefaults onDissapear() to store both arrays into a dictionary. Here's the problem: when I put loadArray(), method that supposed to overwrite vertical and horizontal reading arrays, to onAppear() and revisit the view - none of the stored values are showed( even though, it will print the stored data to console onAppear()). also, when I revisit the view and then start entering the values then sections with stored data are populated... Bound_test.swift. import SwiftUI struct Bound_test: View { let station : String = "Tuscany" var body: some View { NavigationView { VStack { Section { VStack(){ List { Group { NavigationLink(destination: Doors_test(direction: "Outbound"), label: { Text("Out Bound") }) NavigationLink(destination: Doors_test(direction: "Inbound"), label: { Text("In Bound") }) } .padding(.horizontal,1) .padding(.vertical,3) } .pickerStyle(.wheel) } } } .navigationTitle("Select direction") .navigationBarTitleDisplayMode(.inline) } } } struct Bound_test_Previews: PreviewProvider { static var previews: some View { Bound_test() } } Doors.swift import SwiftUI struct Doors_test: View { let station : String = "Tuscany" let direction : String #State var vM = [String](repeating: "", count: 12) //vert reading #State var hM = [String](repeating: "", count: 12) //horiz reading //constructor init(direction: String) { self.direction = direction } #FocusState private var amountIsFocust: Bool var body: some View { Form { //creates 12 doors automatically ForEach(0..<12) { door in Section { HStack { Text ("Vert:") //vert reading TextEditor(text: $vM[door]) .keyboardType(.decimalPad) .focused($amountIsFocust) } HStack { Text ("Horizontal:") //horiz reading TextEditor(text: $hM[door]) .keyboardType(.decimalPad) .focused($amountIsFocust) } } header: { Text ("Door # \(door+1)") } } } .onAppear{ loadArray() } .navigationTitle("Enter measurements") .navigationBarTitleDisplayMode(.inline) .toolbar{ ToolbarItemGroup(placement: .keyboard) { Spacer() //moves Done button to the right of the screen Button("Done") { amountIsFocust = false print(amountIsFocust) } } } .onDisappear { saveArray() //save array on exit } } //yhis method saves all the readings into dict. func saveArray() { UserDefaults.standard.set(vM, forKey: "vReadings \(direction)\(station)") UserDefaults.standard.set(hM, forKey: "hReadings \(direction)\(station)") } //load saved data as an array func loadArray() { vM = UserDefaults.standard.object(forKey: "vReadings \(direction)\(station)") as? [String] ?? [String](repeating: "", count: 12) hM = UserDefaults.standard.object(forKey: "hReadings \(direction)\(station)") as? [String] ?? [String](repeating: "", count: 12) print(vM) print(hM) } } struct Doors_test_Previews: PreviewProvider { static var previews: some View { Doors_test(direction: "Direction") } }
Big thanks for suggesting to use #AppStorage. After some research my code works. import SwiftUI struct Doors: View { // var goingBack : Bool = viewWillDisappear(true) let fileName = Date.now let station : String let direction : String #AppStorage var vM: [String] #AppStorage var hM: [String] init(station: String, direction: String) { self.station = station self.direction = direction //initialize dynamic keys for AppStorage self._vM = AppStorage(wrappedValue:[String](repeating: "", count: 12) , "Vert \(direction) \(station)") self._hM = AppStorage(wrappedValue:[String](repeating: "", count: 12), "Horiz \(direction) \(station)") } #FocusState private var amountIsFocust: Bool var body: some View { Form { ForEach(0..<12) { door in Section { HStack { Text ("Vert:") TextEditor(text: $vM[door]) .keyboardType(.decimalPad) .focused($amountIsFocust) } HStack { Text ("Horizontal:") TextEditor(text: $hM[door]) .keyboardType(.decimalPad) .focused($amountIsFocust) } } header: { Text ("Door # \(door+1)") } } } .onAppear{ loadArray() } .navigationTitle("Enter measurements") .navigationBarTitleDisplayMode(.inline) .toolbar{ ToolbarItemGroup(placement: .keyboard) { Spacer() //moves Done button to the right of the screen Button("Done") { amountIsFocust = false print(amountIsFocust) } } } .onDisappear { saveArray() } } func saveArray() { UserDefaults.standard.set(vM, forKey: "Vert \(direction) \(station)") UserDefaults.standard.set(hM, forKey: "Horiz \(direction) \(station)") } func loadArray() { vM = UserDefaults.standard.object(forKey: "Vert \(direction) \(station)") as? [String] ?? [String](repeating: "", count: 12) hM = UserDefaults.standard.object(forKey: "Horiz \(direction) \(station)") as? [String] ?? [String](repeating: "", count: 12) print(vM) print(hM) } } struct Doors_Previews: PreviewProvider { static var previews: some View { Doors(station: "Station", direction: "Direction") } }
Using Data from Firestore Data Class in reusable picker SwiftUI
I feel like I'm missing something really obvious and I can't seem to figure it out. I want to use a reusable picker in SwiftUI, the one I am referring to is Stewart Lynch's "Reusable-Custom-Picker" https://github.com/StewartLynch/Reusable-Custom-Picker-for-SwiftUI I have tried multiple times to get the filter working with my Firestore data and I am able to get the picker to read the data but then I am unable to filter it. and the reusable picker struct is import Combine import Firebase import SwiftUI struct CustomPickerView: View { #ObservedObject var schoolData = SchoolDataStore() var datas : SchoolDataStore var items : [String] #State private var filteredItems: [String] = [] #State private var filterString: String = "" #State private var frameHeight: CGFloat = 400 #Binding var pickerField: String #Binding var presentPicker: Bool var body: some View { let filterBinding = Binding<String> ( get: { filterString }, set: { filterString = $0 if filterString != "" { filteredItems = items.filter{$0.lowercased().contains(filterString.lowercased())} } else { filteredItems = items } setHeight() } ) return ZStack { Color.black.opacity(0.4) VStack { VStack(alignment: .leading, spacing: 5) { HStack { Button(action: { withAnimation { presentPicker = false } }) { Text("Cancel") } .padding(10) Spacer() } .background(Color(UIColor.darkGray)) .foregroundColor(.white) Text("Tap an entry to select it") .font(.caption) .padding(.leading,10) TextField("Filter by entering text", text: filterBinding) .textFieldStyle(RoundedBorderTextFieldStyle()) .padding() List { ForEach(schoolData.datas, id: \.id) { i in Button(action: { pickerField = i.name withAnimation { presentPicker = false } }) { Text(i.name) } } } } .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(10) .frame(maxWidth: 400) .padding(.horizontal,10) .frame(height: frameHeight) .padding(.top, 40) Spacer() } } .edgesIgnoringSafeArea(.all) .onAppear { filteredItems = items setHeight() } } fileprivate func setHeight() { withAnimation { if filteredItems.count > 5 { frameHeight = 400 } else if filteredItems.count == 0 { frameHeight = 130 } else { frameHeight = CGFloat(filteredItems.count * 45 + 130) } } } } struct CustomPickerView_Previews: PreviewProvider { static let sampleData = ["Milk", "Apples", "Sugar", "Eggs", "Oranges", "Potatoes", "Corn", "Bread"].sorted() static var previews: some View { CustomPickerView(datas: SchoolDataStore(), items: sampleData, pickerField: .constant(""), presentPicker: .constant(true)) } } class SchoolDataStore : ObservableObject{ #Published var datas = [schoolName]() init() { let db = Firestore.firestore() db.collection("School Name").addSnapshotListener { (snap, err) in if err != nil{ print((err?.localizedDescription)!) return } for i in snap!.documentChanges{ let id = i.document.documentID let name = i.document.get("Name") as? String ?? "" self.datas.append(schoolName(id: id, name: name)) } } } } struct schoolName : Identifiable, Codable { var id : String var name : String } I have managed to get the data from Firestore into my picker now, but I am currently unable to filter. When I change the values of the filteredItems into schoolData.datas I get an error about converting to string or .filter is not a member etc. Anybody able to point me in the right direction with this please? Kindest Regards,
How to create fully customizable sections with Binding text?
First of all, sorry for the title which is not precise at all, but I didn't know how else I could title this question. So: 1. What I want to achieve: I want to create an interface where the user can add sections to input different fields, which are fully customizable by the user himself. 2. What I have done so far I was able to create the interface, I can add new sections easily (structured with a "Section Name" on top and a TextField below) and they are customizable, but only in the TextField. They are also deletable, even though I had to do a complicated workaround since the Binding text of the TextField caused the app to crash because the index at which I was trying to remove the item resulted as "out of range". It's not the perfect workaround, but it works, and for now this is the most important thing. When I'll save these sections, I'll save them as an array of Dictionaries where every Dictionary has the section name and its value. However, there's still a few things I wasn't able to do: 3. What I haven't done yet There are still 3 things that I couldn't do yet. First of all, I'd like for the name of the section to be editable. Secondly, I'd like to have the sections that the user adds displayed inside a Form and divided by Sections. As header, I'd like to have each different section name, and grouped inside all the related sections that share the same name. Last but not least, I'd like to allow the user to add multiple TextFields to the same section. I have no idea how to handle this or even if it's possible. 4. Code: ContentView: import SwiftUI struct ContentView: View { #State var editSections = false #State var arraySections : [SectionModel] = [SectionModel(name: "Prova", value: ""), SectionModel(name: "Prova 2", value: ""), SectionModel(name: "Prova", value: "")] #State var showProgressView = false #State var arraySectionsForDeleting = [SectionModel]() #State var openSheetAdditionalSections = false var body: some View { Form { if showProgressView == false { if editSections == false { ForEach(arraySections, id: \.id) { sect in Section(header: Text(sect.name)) { ForEach(arraySections, id: \.id) { sez in if sez.name == sect.name { SectionView(section: sez) } } } } } else { forEachViewSectionsForDeleting } if arraySections.count > 0 { buttoneditSections } } else { loadingView } Section { addSections } } .sheet(isPresented: $openSheetAdditionalSections, content: { AdditionalSectionsSheet(closeSheet: $openSheetAdditionalSections, contView: self) }) } var forEachViewSectionsForDeleting : some View { Section { ForEach(arraySections, id: \.id) { sez in HStack { Text("\(sez.name) - \(sez.value)") .foregroundColor(.black) Spacer() Button(action: { showProgressView = true let idx = arraySections.firstIndex(where: { $0.id == sez.id }) arraySectionsForDeleting.remove(at: idx!) arraySections = [] arraySections = arraySectionsForDeleting showProgressView = false }, label: { Image(systemName: "minus.circle") .foregroundColor(.yellow) }).buttonStyle(BorderlessButtonStyle()) } } } } var buttoneditSections : some View { Button(action: { editSections.toggle() }, label: { Text(editSections == true ? "Done" : "Edit Sections") .foregroundColor(.yellow) }) } var forEachviewSezioniNonModifica : some View { Section { ForEach(arraySections, id: \.id) { sez in Text(sez.name) .foregroundColor(.black) Text(sez.value) .foregroundColor(.black) } } } var addSections : some View { Button(action: { openSheetAdditionalSections = true }, label: { HStack { Text("Add sections") .foregroundColor(.yellow) Spacer() Image(systemName: "plus.circle") .foregroundColor(.yellow) } }) } var loadingView : some View { Section { HStack { Spacer() ProgressView() .progressViewStyle(CircularProgressViewStyle(tint: .black)) Spacer() } } } } struct ContentView_Previews: PreviewProvider { static var previews: some View { ContentView() } } AddSectionSheet and SectionView: import SwiftUI struct AdditionalSectionsSheet: View { #Binding var closeSheet : Bool #Environment(\.colorScheme) var colorScheme var contView : ContentView #Environment(\.presentationMode) var mode: Binding<PresentationMode> #GestureState private var dragOffset = CGSize.zero var body: some View { NavigationView { Form { buttonPhone buttonUrl buttonEmail buttonAddress } .navigationBarTitle(Text("Add section"), displayMode: .inline) .navigationBarBackButtonHidden(true) .navigationBarItems(trailing: Button(action : { closeSheet = false }){ Text("Close") .foregroundColor(.yellow) }) } } var buttonPhone : some View { Button(action: { contView.editSections = false contView.arraySections.append(SectionModel(name: "Phone", value: "")) contView.showProgressView = true closeSheet = false }, label: { HStack { Text("Phone") .foregroundColor(.black) Spacer() } }) } var buttonUrl : some View { Button(action: { contView.editSections = false contView.arraySections.append(SectionModel(name: "URL", value: "")) closeSheet = false }, label: { HStack { Text("URL") .foregroundColor(.black) Spacer() } }) } var buttonAddress : some View { Button(action: { contView.editSections = false contView.arraySections.append(SectionModel(name: "Address", value: "")) contView.showProgressView = true closeSheet = false }, label: { HStack { Text("Address") .foregroundColor(.black) Spacer() } }) } var buttonEmail : some View { Button(action: { contView.editSections = false contView.arraySections.append(SectionModel(name: "Email", value: "")) contView.showProgressView = true closeSheet = false }, label: { HStack { Text("Email") .foregroundColor(.black) Spacer() } }) } } struct SectionView : View { #Environment(\.colorScheme) var colorScheme #ObservedObject var section : SectionModel var body : some View { Section { Text(section.name) .foregroundColor(.black) TextField(section.name, text: self.$section.value) .foregroundColor(.black) } } } SectionModel: import SwiftUI import Combine class SectionModel : Codable, Identifiable, Equatable, ObservableObject, Comparable { var id = UUID() var name : String var value : String init(name: String, value: String) { self.name = name self.value = value } static func == (lhs: SectionModel, rhs: SectionModel) -> Bool { true } static func < (lhs: SectionModel, rhs: SectionModel) -> Bool { true } } I hope I wrote things clear enough to be understood, thanks to everyone who will help!
SwiftUI Animation on property change?
I want to animate the appearance of an item in a list. The list looks like this: Text("Jim") Text("Jonas") TextField("New Player") TextField("New Player") //should be animated when appearing (not shown until a name is typed in the first "New Player") The last TextField should be hidden until newPersonName.count > 0 and then appear with an animation. This is the code: struct V_NewSession: View { #State var newPersonName: String = "" #State var participants: [String] = [ "Jim", "Jonas" ] var body: some View { VStack(alignment: .leading) { ForEach(0...self.participants.count + 1, id: \.self) { i in // Without this if statement, the animation works // but the Textfield shouldn't be shown until a name is typed if (!(self.newPersonName.count == 0 && i == self.participants.count + 1)) { HStack { if(i < self.participants.count) { Text(self.participants[i]) } else { TextField("New Player", text: $newPersonName, onEditingChanged: { focused in if (self.newPersonName.count == 0) { if (!focused) { handleNewPlayerEntry() } } }) } } } } .transition(.scale) Spacer() } } func handleNewPlayerEntry() { if(newPersonName.count > 0) { withAnimation(.spring()) { participants.append(newPersonName) newPersonName = "" } } } } I know withAnimation(...) only applies to participants.append(newPersonName), but how can I get the animation to work on the property change in the if-statement? if ((!(self.newPersonName.count == 0 && i == self.participants.count + 1)).animation()) doesn't work.
Your example code won't compile for me, but here's a trivial example of using Combine inside a ViewModel to control whether a second TextField appears based on a condition: import SwiftUI import Combine class ViewModel : ObservableObject { #Published var textFieldValue1 = "" #Published var textFieldValue2 = "Second field" #Published var showTextField2 = false private var cancellable : AnyCancellable? init() { cancellable = $textFieldValue1.sink { value in withAnimation { self.showTextField2 = !value.isEmpty } } } } struct ContentView: View { #StateObject var viewModel = ViewModel() var body: some View { VStack { TextField("", text: $viewModel.textFieldValue1) .textFieldStyle(RoundedBorderTextFieldStyle()) if viewModel.showTextField2 { TextField("", text: $viewModel.textFieldValue2) .textFieldStyle(RoundedBorderTextFieldStyle()) .transition(.scale) } } } }
Is that approaching what you're attempting to build ? struct ContentView: View { #State var newPersonName: String = "" #State var participants: [String] = [ "Jim", "Jonas" ] #State var editingNewPlayer = false var body: some View { VStack(alignment: .leading, spacing: 16) { ForEach(participants, id: \.self) { participant in Text(participant) .padding(.trailing) Divider() } Button(action: handleNewPlayerEntry, label: { TextField("New Player", text: $newPersonName, onEditingChanged: { edit in editingNewPlayer = edit }, onCommit: handleNewPlayerEntry) }) if editingNewPlayer { Button(action: handleNewPlayerEntry, label: { TextField("New Player", text: $newPersonName) { edit in editingNewPlayer = false } }) } } .padding(.leading) .frame(maxHeight: .infinity, alignment: .top) .transition(.opacity) .animation(.easeIn) } func handleNewPlayerEntry() { if newPersonName.count > 0 { withAnimation(.spring()) { participants.append(newPersonName) newPersonName = "" editingNewPlayer = false } } } }
SwiftUI nested LazyVStacks in a single ScrollView
I'm trying to build a comment thread. So top level comments can all have nested comments and so can they and so on and so forth. But I'm having issues around scrolling and also sometimes when expanding sections the whole view just jumps around, and can have a giant blank space at the bottom. The code looks like this: struct ContentView: View { var body: some View { VStack { HStack { Text("Comments") .font(.system(size: 34)) .fontWeight(.bold) Spacer() } .padding() CommentListView(commentIds: [0, 1, 2, 3], nestingLevel: 1) } } } struct CommentListView: View { let commentIds: [Int]? let nestingLevel: Int var body: some View { if let commentIds = commentIds { LazyVStack(alignment: .leading) { ForEach(commentIds, id: \.self) { id in CommentItemView(viewModel: CommentItemViewModel(commentId: id), nestingLevel: nestingLevel) } } .applyIf(nestingLevel == 1) { $0.scrollable() } } else { Spacer() Text("No comments") Spacer() } } } struct CommentItemView: View { #StateObject var viewModel: CommentItemViewModel let nestingLevel: Int #State private var showComments = false var body: some View { VStack { switch viewModel.viewState { case .error: Text("Error") .fontWeight(.thin) .font(.system(size: 12)) .italic() case .loading: Text("Loading") .fontWeight(.thin) .font(.system(size: 12)) .italic() case .complete: VStack { Text(viewModel.text) .padding(.bottom) .padding(.leading, 20 * CGFloat(nestingLevel)) if let commentIds = viewModel.commentIds { Button { withAnimation { showComments.toggle() } } label: { Text(showComments ? "Hide comments" : "Show comments") } if showComments { CommentListView(commentIds: commentIds, nestingLevel: nestingLevel + 1) } } } } } } } class CommentItemViewModel: ObservableObject { #Published private(set) var text = "" #Published private(set) var commentIds: [Int]? = [0, 1, 2, 3] #Published private(set) var viewState: ViewState = .loading private let commentId: Int private var viewStateInternal: ViewState = .loading { willSet { withAnimation { viewState = newValue } } } init(commentId: Int) { self.commentId = commentId fetchComment() } private func fetchComment() { viewStateInternal = .complete text = CommentValue.allCases[commentId].rawValue } } Has anyone got a better way of doing this? I know List can now accept a KeyPath to child object and it can nest that way, but there's so limited design control over List that I didn't want to use it. Also, while this code is an example, the real code will have to load each comment from an API call, so List won't perform as well as LazyVStack in that regard. Any help appreciated - including a complete overhaul of how to implement this sort of async loading nested view.