In my app, I have a screen where the user can switch filters (of a map) on and off using the Swift Toggle, and also, delete them (with Edit Button). I use a Binding Boolean variable to connect the toggles so that I can react to each change, here is the code:
//The filter data model
class DSFilter{
var filterId : Int
var filterTitle : String
var isActive : Bool
init(filterId : Int, filterTitle : String, isActive : Bool){
self.filterId = filterId
self.filterTitle = filterTitle
self.isActive = isActive
}
}
//The data model representable
struct filterItem : View {
#Binding var filter : DSFilter
#Binding var isOn : Bool
#State var image = Image("defPerson")
var body : some View {
HStack {
image.resizable().frame(width: 45, height: 45).clipShape(Circle())
VStack(alignment: .leading, spacing: 8) {
Text(filter.filterTitle).font(Font.custom("Quicksand-Bold",size: 15))
Text(filter.filterTitle).font(Font.custom("Quicksand-Medium",size: 13)).foregroundColor(.gray)
}.padding(.leading)
Spacer()
HStack{
Toggle("",isOn: $isOn).padding(.trailing, 5).onReceive([self.isOn].publisher.first()) { (value) in
self.filter.isActive = self.isOn
}.frame(width: 30)
}
}.frame(height: 60)
.onAppear(){
self.isOn = self.filter.isActive
}
}
}
//The view where the user has the control
struct FilterView: View {
#State var otherFilters : [addedFilter] = []
#Binding var isActive : Bool
var body: some View {
VStack{
Capsule().fill(Color.black).frame(width: 50, height: 5).padding()
ZStack{
HStack{
Spacer()
Text("Filters").font(Font.custom("Quicksand-Bold", size: 20))
Spacer()
}.padding()
HStack{
Spacer()
EditButton()
}.padding()
}
List{
ForEach(0..<self.otherFilters.count, id:\.self){ i in
filterItem(filter: self.$otherFilters[i].filter, isOn: self.$otherFilters[i].isOn)
}.onDelete(perform:removeRows)
}
Spacer()
}
}
func removeRows(at offsets: IndexSet) {
print("deleting")
print("deleted filter!")
self.otherFilters.remove(at: Array(offsets)[0]) //I use it like so because you can only delete 1 at a time anyway
}
}
//The class I use to bind the isOn value so I can react to change
struct addedFilter : Identifiable{
var id : Int
var filter : DSFilter
var isOn : Bool
init(filter : DSFilter){
self.id = Int.random(in: 0...100000)
self.filter = filter
self.isOn = filter.isActive
}
}
When I use the delete as it is right now I get the following error (Xcode 12.0):
Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
2020-10-06 17:42:12.567235+0300 Hynt[5207:528572] Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444
What do I need to change so I can delete the items? Should I take a different approach to the Binding problem with the Toggle?
Hi instead of keeping three different array to manage addedFilter, DSFilter, isOn. why don't you try the following one single model
class FFilter: Identifiable {
var id: Int
var addedFilter: addedFilter
var dsFilter: DSFilter
var isOn: Bool
init(addedFilter: addedFilter, dsFilter: DSFilter, isOn: Bool) {
self.id = addedFilter.id
self.addedFilter = addedFilter
self.dsFilter = dsFilter
self.isOn = isOn
}
}
And ViewModel change code like this:
class ViewModel: ObservableObject {
#Published var superFilters: [FFilter] = [
FFilter(
addedFilter: addedFilter(
filter: DSFilter(filterId: 1, filterTitle: "Title1", isActive: true)
),
dsFilter: DSFilter(filterId: 1, filterTitle: "Title1", isActive: true),
isOn: true),
FFilter(
addedFilter: addedFilter(
filter: DSFilter(filterId: 2, filterTitle: "Title2", isActive: false)
),
dsFilter: DSFilter(filterId: 2, filterTitle: "Title2", isActive: false),
isOn: false),
FFilter(
addedFilter: addedFilter(filter: DSFilter(filterId: 3, filterTitle: "Title3", isActive: true)),
dsFilter: DSFilter(filterId: 3, filterTitle: "Title3", isActive: true),
isOn: true),
FFilter(
addedFilter: addedFilter(
filter: DSFilter(filterId: 4, filterTitle: "Title4", isActive: true)
),
dsFilter: DSFilter(filterId: 4, filterTitle: "Title4", isActive: true),
isOn: true),
FFilter(
addedFilter: addedFilter(
filter: DSFilter(filterId: 5, filterTitle: "Title5", isActive: false)
),
dsFilter: DSFilter(filterId: 5, filterTitle: "Title5", isActive: false),
isOn: false)
]}
then change your list code
List {
ForEach(viewModel.superFilters) { filter in
filterItem(filter: .constant(filter.dsFilter), isOn: .constant(filter.isOn))
}.onDelete(perform: removeRows(at:)) }
and removeRows method:
func removeRows(at offsets: IndexSet) {
print("deleting at \(offsets)")
self.viewModel.superFilters.remove(atOffsets: offsets)}
I hope it will work. let me know if you still face any issues
I suggest you to change your line in removeRows() function from
self.otherFilters.remove(at: Array(offsets)[0]
to
self.otherFilters.remove(atOffsets: offsets)
So, after a lot of try and fail I came to the answer.
The problem seems to be in the binding part - the compiler can't handle a change in the array size if I bind an element from it so I did the following:
First, I change the filterItem struct so the filter property is #State and not #Binding :
struct filterItem : View {
#State var filter : DSFilter
#Binding var isOn : Bool
Secondly, I have changed the addedFilter struct to the following:
struct addedFilter : Identifiable{
var id : Int
var isOn : Bool
init(filter : DSFilter){
self.id = filter.filterId
self.isOn = filter.isActive
}
}
And the ForEach loop to the following:
ForEach(0..<self.normOthFilters.count, id:\.self){ i in
filterItem(filter: self.normOthFilters[i], isOn: self.$otherFilters[i].isOn)
}.onDelete(perform:removeRows)
To the main view I have added :
#State var normDefFilters : [DSFilter] = []
#State var normOthFilters : [DSFilter] = []
And finally the onDelete function to the following:
func removeRows(at offsets: IndexSet) {
print("deleting")
print("deleted filter!")
self.normOthFilters.remove(atOffsets: offsets)
}
In conclution I am not changing the value of the binded value and so I am not receiving any errors!
Related
I have a problem I change the "isDynamic" setting in the "SettingView" and I exit the setting window and the "SongbookView" does not register that the setting has changed. I want to change the search engine depending on what option is selected in the settings. What is the cause of this situation?
SongbookView:
import CoreData
import SwiftUI
struct SongbookView: View {
#State var searchText: String = ""
#State var isSettings: Bool
#ObservedObject var userSettings: UserSettings = UserSettings()
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Song.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Song.number, ascending: true)]
) var songs: FetchedResults<Song>
var body: some View {
NavigationView{
VStack{
if userSettings.isDynamic == false {
SearchBar(text: $searchText)
} else {
DynamicSearchBar(text: $searchText)
}
List(songs.filter({searchText.isEmpty ? true : removeNumber(str: $0.content!.lowercased()).contains(searchText.lowercased()) || String($0.number).contains(searchText)}), id:\.objectID) { song in
NavigationLink(destination: DetailView(song: song, isSelected: song.favorite)) {
HStack{
Text("\(String(song.number)). ") .font(.headline) + Text(song.title ?? "Brak tytułu")
if song.favorite {
Spacer()
Image(systemName: "heart.fill")
.accessibility(label: Text("To jest ulubiona pieśń"))
.foregroundColor(.red)
}
}.lineLimit(1)
}
}.id(UUID())
.listStyle(InsetListStyle())
}
.padding(.top, 10)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .principal) {
HStack{
Text(String(self.userSettings.isDynamic))
Spacer()
Text("Śpiewnik")
.font(.system(size: 20))
.bold()
Spacer()
Button(action: {
isSettings.toggle()
print(userSettings.isDynamic)
}) {
Image(systemName: "gearshape")
.resizable()
.frame(width: 16.0, height: 16.0)
}
.sheet(isPresented: $isSettings) {
SettingView(isPresented: $isSettings)
}
}
}
}
}
}
func removeNumber(str: String) -> String {
var result = str
let vowels: Set<Character> = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]
result.removeAll(where: { vowels.contains($0) })
return result
}
}
SettingView:
import SwiftUI
struct SettingView: View {
#ObservedObject var userSettings = UserSettings()
#Binding var isPresented: Bool
var body: some View {
NavigationView {
Form {
Toggle("Dynamiczna wyszukiwarka", isOn: $userSettings.isDynamic)
.onChange(of: userSettings.isDynamic) { value in
print(value)
}
Button(action: {
print(userSettings.isDynamic)
isPresented = false
}) {
Text("Test")
}
}
.navigationBarTitle("Ustawienia")
}
}
}
UserSettings:
import Foundation
import Combine
class UserSettings: ObservableObject {
#Published var isDynamic: Bool {
didSet {
UserDefaults.standard.set(isDynamic, forKey: "isSearchDynamic")
}
}
init() {
self.isDynamic = UserDefaults.standard.object(forKey: "isSearchDynamic") as? Bool ?? false
}
}
You're using two different instances of UserSettings. When you update isDynamic on one of those instances, the other, even though it has a reference to UserDefaults has no reason to know that it needs to update.
The easiest solution here is to share a single instance of UserSettings:
struct SongbookView: View {
#State var searchText: String = ""
#State var isSettings: Bool
#ObservedObject var userSettings: UserSettings = UserSettings()
#Environment(\.managedObjectContext) var managedObjectContext
#FetchRequest(
entity: Song.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \Song.number, ascending: true)]
) var songs: FetchedResults<Song>
var body: some View {
//...
.sheet(isPresented: $isSettings) {
SettingView(userSettings: userSettings,isPresented: $isSettings)
}
//...
}
}
struct SettingView: View {
#ObservedObject var userSettings : UserSettings
#Binding var isPresented: Bool
var body: some View {
//...
}
}
You could also look into property wrappers like #AppStorage (https://www.hackingwithswift.com/quick-start/swiftui/what-is-the-appstorage-property-wrapper) that actually update dynamically when the UserDefaults values change.
I have a list of entries each with an attached date. I would like to display the date only if there is a change in date. I first developed this software in iOS 14.4 that resulted in a view immutable error. This was because I was storing and changing a copy of the entry date.
Now in version iOS 14.5 I don't see the immutable error. But my software still doesn't work. If you run the code and look in the console you will note that Xcode is going through my six entries twice: the first time is always true (show the date) and the second time always false (don't show the date). Why?
In my actual code I am using dates of type Date instead of Strings in this example code. In my actual code, operation hangs as it loops endlessly through my function checkDate (Many times more than the number of entries). Does date of type Date include the time causing the compare to fail?
Is there a better way to prevent display of the date if it is the same as the previous entry?
struct KitchenItem: Codable, Identifiable {
var id = UUID()
var item: String
var itemDate: String
var itemCost: Double
}
class Pantry: ObservableObject {
#Published var oldDate: String = ""
#Published var kitchenItem: [KitchenItem]
init() {
self.kitchenItem = []
let item0 = KitchenItem(item: "String Beans", itemDate: "1/13/2021", itemCost: 4.85)
self.kitchenItem.append(item0)
let item1 = KitchenItem(item: "Tomatoes", itemDate: "1/22/2021", itemCost: 5.39)
self.kitchenItem.append(item1)
let item2 = KitchenItem(item: "Bread", itemDate: "1/22/2021", itemCost: 4.35)
self.kitchenItem.append(item2)
let item3 = KitchenItem(item: "Corn", itemDate: "3/18/2021", itemCost: 2.75)
self.kitchenItem.append(item3)
let item4 = KitchenItem(item: "Peas", itemDate: "3/18/2021", itemCost: 7.65)
self.kitchenItem.append(item4)
let item5 = KitchenItem(item: "Ice Cream", itemDate: "4/12/2021", itemCost: 7.95)
self.kitchenItem.append(item5)
}
}
struct ContentView: View {
#ObservedObject var pantry: Pantry = Pantry()
var body: some View {
LazyVStack (alignment: .leading) {
Text("Grandma's Food Pantry")
.font(.title)
.fontWeight(.bold)
.padding(.top, 36)
.padding(.leading, 36)
.padding(.bottom, 30)
ForEach(0..<pantry.kitchenItem.count, id: \.self) { item in
VStack (alignment: .leading) {
showRow(item: item)
}
}
}
}
}
struct showRow: View {
#ObservedObject var pantry: Pantry = Pantry()
var item: Int
var body: some View {
// don't show the date if is the same as the previous entry
let newDate = pantry.kitchenItem[item].itemDate
if checkDate(newDate: newDate) == true {
Text("\n\(newDate)")
.font(.title2)
.padding(.leading, 10)
}
HStack {
Text("\(pantry.kitchenItem[item].item)")
.padding(.leading, 50)
.frame(width: 150, alignment: .leading)
Text("\(pantry.kitchenItem[item].itemCost, specifier: "$%.2f")")
}
}
func checkDate(newDate: String) -> (Bool) {
print(" ")
print("new date = \(newDate)")
if newDate == pantry.oldDate {
print("false: don't show the date")
return false
} else {
pantry.oldDate = newDate
print("old date = \(pantry.oldDate)")
print("true: show the date")
return true
}
}
}
Actual code:
struct ListView: View {
#EnvironmentObject var categories: Categories
#EnvironmentObject var userData: UserData
#Environment(\.managedObjectContext) var viewContext
var money: String = ""
var xchRate: Double = 0.0
var cat: Int = 0
var mny: String = ""
#FetchRequest(
entity: CurrTrans.entity(),
sortDescriptors: [NSSortDescriptor(keyPath: \CurrTrans.entryDT, ascending: true)]
) var currTrans: FetchedResults<CurrTrans>
var body: some View {
GeometryReader { g in
ScrollView {
LazyVStack (alignment: .leading) {
TitleView()
ForEach(currTrans, id: \.self) { item in
showRow(item: item, priorDate: priorDate(forItemIndex: item), g: g)
}
.onDelete(perform: deleteItem)
}
.font(.body)
}
}
}
private func priorDate(forItemIndex item: Int) -> Date? {
guard item > 0 else { return nil }
return currTrans[item - 1].entryDT
}
}
struct showRow: View {
#EnvironmentObject var userData: UserData
var item: CurrTrans
var priorDate: Date?
var g: GeometryProxy
var payType = ["Cash/Debit", "Credit"]
var body: some View {
// don't show the date if is the same as the previous entry
let gotDate = item.entryDT ?? Date()
let newDate = gotDate.getFormattedDate()
Text("\(newDate)")
.opacity(gotDate == priorDate ? 0 : 1)
.font(.title2)
.padding(.leading, 10)
displays entry parameters in HStack...
Thou shalt not mutate thy data inside thy body method, for it is an abomination in the eyes of SwiftUI.
Modifying oldDate inside the body method is wrong. SwiftUI will get confused if you modify the data it is observing while it is rendering your views. Furthermore, SwiftUI doesn't make any guarantees about the order in which it renders the children of a LazyVStack (or any other container).
Is there a better way to prevent display of the date if it is the same as the previous entry?
Yes. Pass the current entry, and the prior entry's date, to the entry view.
Here's your data model and store, without the cruft:
struct KitchenItem: Codable, Identifiable {
var id = UUID()
var item: String
var itemDate: String
var itemCost: Double
}
class Pantry: ObservableObject {
#Published var kitchenItems: [KitchenItem] = [
.init(item: "String Beans", itemDate: "1/13/2021", itemCost: 4.85),
.init(item: "Tomatoes", itemDate: "1/22/2021", itemCost: 5.39),
.init(item: "Bread", itemDate: "1/22/2021", itemCost: 4.35),
.init(item: "Corn", itemDate: "3/18/2021", itemCost: 2.75),
.init(item: "Peas", itemDate: "3/18/2021", itemCost: 7.65),
.init(item: "Ice Cream", itemDate: "4/12/2021", itemCost: 7.95),
]
}
For each KitchenItem, you need to also extract the prior item's date, if there is a prior item. We'll use a helper method, priorDate(forItemIndex:), to do that. Also, you need to use StateObject, not ObservedObject, if you're going to create your store inside the view. Thus:
struct ContentView: View {
#StateObject var pantry: Pantry = Pantry()
var body: some View {
ScrollView(.vertical) {
LazyVStack (alignment: .leading) {
Text("Grandma's Food Pantry")
.font(.title)
.fontWeight(.bold)
.padding(.top, 36)
.padding(.leading, 36)
.padding(.bottom, 30)
ForEach(0 ..< pantry.kitchenItems.count) { i in
if i > 0 {
Divider()
}
KitchenItemRow(item: pantry.kitchenItems[i], priorDate: priorDate(forItemIndex: i))
}
}
}
}
private func priorDate(forItemIndex i: Int) -> String? {
guard i > 0 else { return nil }
return pantry.kitchenItems[i - 1].itemDate
}
}
Here is KitchenItemRow. You can see that it makes the date Text transparent if the date is the same as the prior item's date. I keep it in place but make it transparent so the row lays out the same:
struct KitchenItemRow: View {
var item: KitchenItem
var priorDate: String?
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(item.item)
Spacer()
Text("\(item.itemCost, specifier: "$%.2f")")
}
Text(item.itemDate)
.opacity(item.itemDate == priorDate ? 0 : 1)
}
.padding([.leading, .trailing], 10)
}
}
And here is TitleView, extracted from ContentView for hygiene:
struct TitleView: View {
var body: some View {
Text("Grandma's Food Pantry")
.font(.title)
.fontWeight(.bold)
.padding(.top, 36)
.padding(.leading, 36)
.padding(.bottom, 30)
}
}
Result:
UPDATE
Since your “real code” uses onDelete, it's important to give ForEach an id for each item instead of using the indexes.
Note that onDelete only works inside List, not inside LazyVStack.
So we need to map each item to its index, so we can find the prior item. Here's a revised version of my ContentView that uses onDelete:
struct ContentView: View {
#StateObject var pantry: Pantry = Pantry()
var body: some View {
let indexForItem: [UUID: Int] = .init(
uniqueKeysWithValues: pantry.kitchenItems.indices.map {
(pantry.kitchenItems[$0].id, $0) })
List {
TitleView()
ForEach(pantry.kitchenItems, id: \.id) { item in
let i = indexForItem[item.id]!
KitchenItemRow(item: item, priorDate: priorDate(forItemIndex: i))
}
.onDelete(perform: deleteItems(at:))
}
}
private func priorDate(forItemIndex i: Int) -> String? {
guard i > 0 else { return nil }
return pantry.kitchenItems[i - 1].itemDate
}
private func deleteItems(at offsets: IndexSet) {
pantry.kitchenItems.remove(atOffsets: offsets)
}
}
In my testing, this works and allows swipe-to-delete. I trust you can adapt it to your “real” code.
Console Bug: SwiftUI encountered an issue when pushing aNavigationLink. Please file a bug.
There is no problem when I don't use the isActive parameter in NavigationLink. However, I have to use the isActive parameter. Because I'm closing the drop-down list accordingly.
Menu Model:
struct Menu: Identifiable {
var id: Int
var pageName: String
var icon: String
var page: Any
var startDelay: Double
var endDelay: Double
// var offsetY: CGFloat
}
let menu = [
Menu(id: 1, pageName: "Profil", icon: "person.crop.circle", page: ProfileView(), startDelay: 0.2, endDelay: 0.6),
Menu(id: 2, pageName: "Sepet", icon: "cart", page: CartView(), startDelay: 0.4, endDelay: 0.4),
Menu(id: 3, pageName: "İstek", icon: "plus.circle", page: ClaimView(), startDelay: 0.6, endDelay: 0.2)
]
MenuView
struct MenuView: View {
#State var isShownMenu: Bool = false
#State var isPresented: Bool = false
var body: some View {
VStack(spacing: 40) {
Button(action: {self.isShownMenu.toggle()}) {
MenuViewButton(page: .constant((Any).self), icon: .constant("rectangle.stack"))
}
VStack(spacing: 40) {
ForEach(menu, id: \.id) { item in
NavigationLink(
destination: AnyView(_fromValue: item.page),
isActive: self.$isPresented,
label: {
MenuViewButton(page: .constant(item.page), icon: .constant(item.icon))
.animation(Animation.easeInOut.delay(self.isShownMenu ? item.startDelay : item.endDelay))
.offset(x: self.isShownMenu ? .zero : UIScreen.main.bounds.width)//, y: item.offsetY)
}
}
.onChange(of: isPresented, perform: { value in
if value == true {
self.isShownMenu = false
}
})
}
}
}
The problem is that you have NavigationLink with the "IsActive" parameter placed in the ForEach cycle!
You need to remove NavigationLink from the cycle and transfer the necessary data there, for example, through the view model.
Summary: you should only have one NavigationLink associated with one specific isActive parameter.
ForEach(yourData) { dataItem in
Button {
selectedItem = dataItem
isActivated = true
} label: {
Text("\(dataItem)")
}
}
.background(
NavigationLink(destination: DestinationView(data: selectedItem),
isActive: $isActivated) {EmptyView()}
)
ForEach(items) { item in
NavigationLink(tag: item.id, selection: $selection) {
DetailView(selection: $selection, item: item)
} label: {
Text("\(item)")
}
}
I got this when I accidentally had two NavigationLinks with the same isActive boolean.
I have a view that includes a ForEach, and I have a button that adds more items to the list. In each instance, in the loop, I have a TextField that is pre-filled with an autogenerated counter name. But when I add a new instance, all the previously added items change the name to the same:
What I would like is to have the counter names be Counter 1 for the first, the second Counter 2, third Counter 3, etc.
Here's my code:
import SwiftUI
struct Counter: Identifiable, Equatable {
var id: UUID = UUID()
var name: String = ""
var rows: Int = 0
var repeats: Int?
var rowsPerRepeat: Int?
var countRepeats: Bool = false
}
struct Project: Identifiable, Equatable {
var id:UUID = UUID()
var name:String = ""
var counters: [Counter] = [Counter]()
}
class AddEditCounterViewModel : ObservableObject {
#Published var counter : Counter
#Published var project: Project
init(counter: Counter, project: Project) {
self.project = project
self.counter = counter
if self.counter.name.isEmpty {
self.counter.name = counterNameGenerator()
}
}
func counterNameGenerator() -> String {
let count = project.counters.count
return String.localizedStringWithFormat(NSLocalizedString("Counter %d", comment: "Counter name"), count)
}
func countRepeats(countRepeats : Bool) {
if countRepeats {
counter.countRepeats = true
if counter.rowsPerRepeat == nil {
counter.rowsPerRepeat = 2
}
} else {
counter.countRepeats = false
counter.rowsPerRepeat = nil
}
}
}
struct AddEditCounterView: View {
#ObservedObject var viewModel : AddEditCounterViewModel
#State var countRepeats = false
#State var hiddenHeight : CGFloat = 0.0
#State var opacity = 0.0
init(viewModel: AddEditCounterViewModel) {
self.viewModel = viewModel
}
var body: some View {
VStack(spacing: 20) {
VStack (alignment: .leading) {
Text("Counter Name")
.multilineTextAlignment(.leading)
TextField("", text: $viewModel.counter.name)
}
HStack {
Text("Start at")
.multilineTextAlignment(.leading)
Spacer()
TextField("", value: $viewModel.counter.rows, formatter: NumberFormatter())
.keyboardType(.numberPad)
.multilineTextAlignment(.center)
.frame(width: 70.0, height: nil, alignment: .leading)
}.frame(maxWidth: .infinity, alignment: .leading)
Toggle(isOn: $countRepeats) {
Text("Count sets (repeats)")
}.onChange(of: countRepeats, perform: { value in
viewModel.countRepeats(countRepeats: value)
hiddenHeight = value ? 30.0 : 0
opacity = value ? 1 : 0
})
HStack {
Text("How many rows per set (repeat)")
.multilineTextAlignment(.leading)
Spacer()
TextField("", value: $viewModel.counter.rowsPerRepeat, formatter: NumberFormatter())
.keyboardType(.numberPad)
.multilineTextAlignment(.center)
.frame(width: 70.0, height: nil, alignment: .leading)
}
.opacity(opacity)
.frame(maxWidth: .infinity, maxHeight: $hiddenHeight.wrappedValue, alignment: .leading)
.animation(.easeIn)
}
}
}
class AddEditProjectViewModel: ObservableObject {
#Published var project : Project
init(project: Project) {
self.project = project
if self.project.counters.count < 1 {
addNewCounter()
}
}
func addNewCounter() {
project.counters.append(Counter())
}
}
struct AddEditProjectView: View {
#ObservedObject var viewModel : AddEditProjectViewModel
var body: some View {
VStack {
ForEach(viewModel.project.counters) { counter in
AddEditCounterView(viewModel: AddEditCounterViewModel(counter: viewModel.project.counters[0], project: viewModel.project))
}
Button( action: {
viewModel.addNewCounter()
}){
Text("Add Counter")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
AddEditProjectView(viewModel: AddEditProjectViewModel(project: Project()))
}
}
i have a small swiftUI programm in Xcode which let me create and delete Users in a list with a stepper to count points of the users.
everything works fine (adding users, renaming users, stepper counting) unless the deletion of the user.
it throws an error:
Fatal error: Index out of range: file
/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.8/swift/stdlib/public/core/ContiguousArrayBuffer.swift,
line 444 2020-05-23 12:06:22.854920+0200 Counter[21328:1125981] Fatal
error: Index out of range: file
/Library/Caches/com.apple.xbs/Sources/swiftlang/swiftlang-1103.2.25.8/swift/stdlib/public/core/ContiguousArrayBuffer.swift,
line 444
Here is the code:
import SwiftUI
struct ContentView : View {
#State var isEditing = false
#State var stepperWerte = [3, 5, 7, 9]
#State var editText = ["Player 1", "Player 2", "Player 3", "Player 4"]
var startName = "new Player"
var startLeben = 5
var body: some View {
NavigationView {
List() {
ForEach(0..<editText.count, id: \.self) {
spieler in HStack {
if self.editText.indices.contains(spieler) {
Stepper(value: self.$stepperWerte[spieler], in: -1...10, step: 1, label: {
TextField("", text: self.$editText[spieler], onEditingChanged: {_ in }, onCommit: {self.saveText(id: spieler, Text: self.editText[spieler])} )
.layoutPriority(1)
.fixedSize(horizontal: true, vertical: false)
Text("\(self.stepperWerte[spieler]) - \(spieler) - \(self.editText.count)")})
}
}
}
.onDelete(perform: spielerLoeschen)
.frame(width: nil, height: nil, alignment: .trailing)
}
.navigationBarTitle(Text("Nav_Title"))
.navigationBarItems(leading: Button(action: { self.isEditing.toggle() }) { Text(isEditing ? "Done" : "Edit").frame(width: 85, height: 40, alignment: .leading) },
trailing: Button(action: spielerHinzufuegen, label: { Image(systemName: "plus") }) )
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
}
}
func spielerLoeschen(at offsets: IndexSet) {
stepperWerte.remove(atOffsets: offsets)
editText.remove(atOffsets: offsets)
}
func spielerHinzufuegen() {
stepperWerte.append(startLeben)
editText.append(startName)
}
func saveText(id: Int, Text: String) {
editText[id] = Text
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
(ignore the "if" after the HStack, it has no real effect and those extra prints in the last Text to show the index and the total count)
if i dump the arrays (stepperWerte and editText) they are removed the right way -> the player selected for deletion will be removed correctly from the two arrays.
if i change
TextField("", text: self.$editText[spieler]
to
TextField("", text: self.$editText[0]
it works (unless naturally it displays the first player in all rows and i got the same error after deleting all the players (=rows))
any help would be great - thank you!
According to #Asperi i have changed my code to the following:
import SwiftUI
struct BetterTextField : View {
var container: Binding<[String]>
var index: Int
#State var text: String
var body: some View {
TextField("", text: self.$text, onCommit: {
self.container.wrappedValue[self.index] = self.text
})
.layoutPriority(1)
.fixedSize(horizontal: true, vertical: false)
}
}
struct ContentView : View {
#State var isEditing = false
#State var stepperWerte = [3, 5, 7, 9]
#State var editText = ["Player 1", "Player 2", "Player 3", "Player 4"]
var startName = "new Player"
var startLeben = 5
var body: some View {
NavigationView {
List() {
ForEach(0..<editText.count, id: \.self) {
spieler in HStack {
if self.editText.indices.contains(spieler) {
Stepper(value: self.$stepperWerte[spieler], in: -1...10, step: 1, label: {
BetterTextField(container: self.$editText, index: self.editText.firstIndex(of: self.editText[spieler])!, text: self.editText[spieler])
Text("\(self.stepperWerte[spieler]) - \(spieler) - \(self.editText.count)")})
}
}
}
.onDelete(perform: spielerLoeschen)
.frame(width: nil, height: nil, alignment: .trailing)
}
.navigationBarTitle(Text("Nav_Title"))
.navigationBarItems(leading: Button(action: { self.isEditing.toggle() }) { Text(isEditing ? "Done" : "Edit").frame(width: 85, height: 40, alignment: .leading) },
trailing: Button(action: spielerHinzufuegen, label: { Image(systemName: "plus") }) )
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
}
}
func spielerLoeschen(at offsets: IndexSet) {
stepperWerte.remove(atOffsets: offsets)
editText.remove(atOffsets: offsets)
}
func spielerHinzufuegen() {
stepperWerte.append(startLeben)
editText.append(startName)
}
func saveText(id: Int, Text: String) {
editText[id] = Text
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
... and it works - thank you!
but:
is this a bug in SwiftUI or intentional?
The problem is that you are not using your items directly in the ForEach loop. Consider using structs for your data as objects and make them identifiable.
struct Player : Identifiable {
let id = UUID()
var stepperWerte: Int
var editText : String
}
struct ContentView : View {
#State var isEditing = false
#State var players = [Player(stepperWerte: 3, editText: "Player 1"), Player(stepperWerte: 5, editText: "Player 2"), Player(stepperWerte: 7, editText: "Player 3"), Player(stepperWerte: 9, editText: "Player 4")]
var startName = "new Player"
var startLeben = 5
var body: some View {
NavigationView {
List() {
ForEach(self.players) { player in
SecondView(player: player)
}
.onDelete(perform: spielerLoeschen)
}
.frame(width: nil, height: nil, alignment: .trailing)
.navigationBarTitle(Text("Nav_Title"))
.navigationBarItems(leading: Button(action: { self.isEditing.toggle() }) { Text(isEditing ? "Done" : "Edit").frame(width: 85, height: 40, alignment: .leading) },
trailing: Button(action: spielerHinzufuegen, label: { Image(systemName: "plus") }) )
.environment(\.editMode, .constant(self.isEditing ? EditMode.active : EditMode.inactive)).animation(Animation.spring())
}
}
func spielerLoeschen(at offsets: IndexSet) {
players.remove(atOffsets: offsets)
}
func spielerHinzufuegen() {
players.insert(Player(stepperWerte: 4, editText: "Neuer Player"), at: 0)
}
}
struct SecondView : View {
var player : Player
#State var stepperWerte : Int
#State var name : String
init(player : Player)
{
self._stepperWerte = State(initialValue: player.stepperWerte)
self._name = State(initialValue: player.editText)
self.player = player
}
var body: some View
{
Stepper(value: self.$stepperWerte, in: -1...10, step: 1, label: {
TextField("", text: self.$name)
.layoutPriority(1)
.fixedSize(horizontal: true, vertical: false)
Text("\(player.stepperWerte)")
})
}
}
I created a struct Player, and then an array of many Players. In the ForEach you can directly use players as Player confirms to Identifiable protocol. This is way easier as you can access a player object in your ForEach loop and you do not have to access everything with indices. In the deleting function you just delete the object out of the array or add something new to it. Deleting now works fine.
I have removed some code from the list row, just to reproduce it easier, just if you are wondering.