SwiftUI custom nested array ForEach - arrays

So i have a nested array of a model:
let models = [[ButtonModel]]
struct ButtonModel: Identifiable {
let id = UUID()
let value: String
let style: ColorStyle
init(_ value: String, _ style: ColorStyle) {
self.value = value
self.style = style
}
}
Then i want to add this as a grid so i have a VStack in which i loop x amount of HStacks with buttons in it.
But because of some reason i get this error:
Cannot convert value of type '[[ButtonModel]]' to expected argument type 'Binding'
Generic parameter 'C' could not be inferred
VStack {
ForEach(viewModel.buttons, id: \.self) { buttons in
HStack(spacing: GridPoints.x2) {
Spacer()
ForEach(buttons) { buttonValue in
if buttonValue == "/" {
imageButton(for: Asset.Image) { viewModel.addValue(buttonValue) }
} else {
Button(buttonValue, action: { viewModel.addValue(buttonValue) })
.buttonStyle(customFont: .h3)
.background(Color.backgroundColor)
.styleText(style: TextStyle.h3)
}
}
Spacer()
}
.padding(GridPoints.x1)
}
}
Anyone know what this error is?

Edit
It looks like you made a mistake when using buttonValue too.
You should use it like this
ForEach(buttons) { button in
if button.value == "/" {
imageButton(for: Asset.Image) { viewModel.addValue(button.value) }
}
...
}
ForEach requires an array whose every element is Identifiable (or telling which one is the id to use using id: \.something).
Your problem is that if you use an array of array, this means that every element of the outer array is another array which is NOT conforming to Identifiable.
If you use ForEach(arrayOfArray, id: \.self), on the other hand, your telling that the identifier will be each element of the outer array that must conform to Hashable. Array conform to Hashable if its element does. So try to change your struct to
struct ButtonModel: Identifiable, Hashable { ... }
Alternate solution
You could change your structure to
struct ButtonsGroup: Identifiable {
let id = UUID()
let buttons: [ButtonModel]
}
Then you will be using it like this:
let groups = [ButtonsGroup]()
ForEach(groups) { group in
ForEach(group.buttons) { button in
Text(button.value)
}
}

Related

SwiftUI remove data into a structured array via a button in a ForEach

I'm trying to remove a line in my structured array when user click on the delete button. But as I use a foreach to load all my array lines into a specific subview I don't know how to pass the index of the ForEach into my subview to delete my line...
My code is like this,
ScrollView{
VStack {
ForEach(planeLibrary.testPlane){plane in
ZStack {
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(Color.white)
.shadow(color: Color(Color.RGBColorSpace.sRGB, white: 0, opacity: 0.2), radius: 4)
PlaneCellView(plane: plane, planeLibrary: planeLibrary, line: ???)
}
}
}.padding(.horizontal, 16)
}
And my PlaneCellView :
#State var plane: Plane
#ObservedObject var planeLibrary: PlaneLibrary
var line: Int
var body: some View {
//...
VStack(alignment: .leading) {
Text(plane.planeImat)
.font(.title)
.fontWeight(.bold)
Text(plane.planeType)
HStack{
Text(plane.isSe ? "SE" : "ME")
Text(plane.isNight ? "- Night" : "")
Text(plane.isIfr ? "- IFR" : "")
}
}
Spacer()
Button {
// HERE I don't know how to delete my array line ...
planeLibrary.testPlane.remove(at: line)
} label: {
Image(systemName: "trash.circle")
.foregroundColor(.red)
.font(.system(size: 30))
}
//...
}
My Plane library :
struct Plane: Identifiable{
let id = UUID().uuidString
let planeImat: String
let planeType: String
let isSe: Bool
let isIfr: Bool
let isNight: Bool
let autoID: String
init (planeImat: String, planeType: String, isSe: Bool, isIfr: Bool, isNight: Bool, autoID: String){
self.planeType = planeType
self.planeImat = planeImat
self.isSe = isSe
self.isIfr = isIfr
self.isNight = isNight
self.autoID = autoID
}
init(config: NewPlaneConfig){
self.planeImat = config.imat
self.planeType = config.type
self.isSe = config.isSe
self.isIfr = config.isIfr
self.isNight = config.isNight
self.autoID = config.autoID
}
}
I've already try to add id: \.self as I was able to find on this forum but without any success.
You haven't actually included PlaneLibrary, so I will assume that planeLibrary.testPlane is an array of Plane structs.
There are many ways of solving this, including changing testPlane to be a Dictionary of Plane structs (indexed by id), or if order is important, in an OrderedDictionary (add the swift-collections package to your project and import OrderedCollections in the file where it is used). You could use testPlane.removeValue(at: id) to remove the plane from either type of dictionary.
If you keep it as an array, but your array might be large and you're worried about run-time efficiency, the best thing to do is to change your ForEach to include the index of the planes in the loop.
It would look something like this:
ForEach(Array(planeLibrary.testPlane.enumerated()), id: \.element.id) { index, plane in
// In this code you can use either plane, or index.
...
// UI code
Text(plane.autoID)
...
{ // remove closure
planeLibrary.testPlane.remove(at: index)
}
}
But if the array is of reasonable size, you could keep it as it is now and use testPlane.remove(where:) to find it by id at the time of deletion. The code for this is much simpler and easier to read and understand, so it should probably be your first choice. Optimise for large lists later, if you need.
You can't pass the index in because that will crash the ForEach View. Instead, look up its index using its ID afterwards to remove it, e.g.
class RecipeBox: ObservableObject {
#Published var allRecipes: [Recipe]
#Published var collections: [String]
...
func delete(_ recipe: Recipe) {
delete(recipe.id)
}
func delete(_ id: Recipe.ID) {
if let index = index(for: id) {
allRecipes.remove(at: index)
updateCollectionsIfNeeded()
}
}
...
func index(for id: Recipe.ID) -> Int? {
allRecipes.firstIndex(where: { $0.id == id })
}
...
This sample is from Defining the source of truth using a custom binding (Apple Developer)

"Thread 1: Fatal error: Index out of range" when removing from array with #Binding

I have run into this issue in SwiftUI. I want to be able to remove an item from an Array when the user presses on a button, but I get a "Thread 1: Fatal error: Index out of range" error when I try. This seems to have to do with the fact that IntView takes in a #Binding: if I make num just a regular variable, the code works fine with no errors. Unfortunately, I need to be able to pass in a Binding to the view for my purposes (this is a simplified case), so I am not sure what I need to do so the Binding doesn't cause the bug.
Here is my code:
import SwiftUI
struct IntView: View {
#Binding var num: Int // if I make this "var num: Int", there are no bugs
var body: some View {
Text("\(num)")
}
}
struct ArrayBugView: View {
#State var array = Array(0...10)
var body: some View {
ForEach(array.indices, id: \.self) { num in
IntView(num: $array[num])
Button(action: {
self.array.remove(at: num)
}, label: {
Text("remove")
})
}
}
}
Any help is greatly appreciated!
In your code the ForEach with indicies and id: \.self is a mistake. The ForEach View in SwiftUI isn’t like a traditional for loop. The documentation of ForEach states:
/// It's important that the `id` of a data element doesn't change, unless
/// SwiftUI considers the data element to have been replaced with a new data
/// element that has a new identity.
This means we cannot use indices, enumerated or a new Array in the ForEach. The ForEach must be on the actual array of identifiable items. This is so SwiftUI can track the row Views moving around, which is called structural identity and you can learn about it in Demystify SwiftUI WWDC 2021.
So you have to change your code to something this:
import SwiftUI
struct Item: Identifiable {
let id = UUID()
var num: Int
}
struct IntView: View {
let num: Int
var body: some View {
Text("\(num)")
}
}
struct ArrayView: View {
#State var array: [Item] = [Item(num:0), Item(num:1), Item(num:2)]
var body: some View {
ForEach(array) { item in
IntView(num: item.num)
Button(action: {
if let index = array.firstIndex(where: { $0.id == item.id }) {
array.remoteAt(index)
}
}, label: {
Text("remove")
})
}
}
}

SwiftUI - Index out of range

I start by adding some integers to an array in onAppear for my outermost stack. But when I try to display the contents of the array using ForEach, I get an index out of range error.
struct MyView: View {
#State private var answers = [Int]()
var body: some View {
VStack {
ForEach(0..<4) { number in
Text("\(answers[number])")
}
}
.onAppear {
for _ in (0..<4) {
answerArray.append(Int.random(in: 1...10))
}
}
Never retrieve items by hard-coded indices in a ForEach expression.
Do count the array, the loop is skipped (safely) if the array is empty.
ForEach(0..<answers.count) { number in
Or - still simpler – enumerate the items rather than the indices
ForEach(answers, id: \.self) { answer in
Text("\(answer)")
}
onAppear is called after MyView loads for the first time, and at that moment, answers is still empty. That's why your program crashes at ForEach(0..<4), because answers's doesn't have 4 elements yet.
ForEach(0..<4) { number in
Text("\(answers[number])") /// answers is still empty.
}
Instead, you should look over answers.indices, so that answers[number] is guaranteed to exist. Make sure to also provide an id (id: \.self) to satisfy ForEach's Identifiable requirement.
struct MyView: View {
#State private var answers = [Int]()
var body: some View {
VStack {
ForEach(answers.indices, id: \.self) { number in
Text("\(answers[number])")
}
}
.onAppear {
for _ in (0..<4) {
answers.append(Int.random(in: 1...10)) /// you probably meant `answers.append`, not `answerArray.append`
}
}
}
}

How we can achieve this Filter in Swift

How we can achieve this Filter in Swift.
I have exactly same problem and i am trying this way and i found this solution on stack overflow
but this is written in Javascript and i need code in Swift language.
Getting this error
Cannot convert value of type '[Model]' to closure result type
'GetModel'
My Code and Model
extension Array where Element == GetModel{
func matching(_ text: String?) -> [GetModel] {
if let text = text, text.count > 0{
return self.map{
$0.data.filter{
$0.name.lowercased().contains(text.lowercased())
}
}
}else{
return self
}
}
}
// MARK: - GetModel
struct GetModel: Codable {
let id: Int
let name: String
var data: [Model]
}
// MARK: - Model
struct Model:Codable {
let id: Int
let name: String
var isSelected: Bool? = nil
}
You are making two mistakes. First you are using map but you should be using filter. Second you are using filter when you should be using contains(where:). Note you can. use localizedStandardCompare instead of lowercasing your string.
Note: You shouldn't check if your string count is greater than zero. String has an isEmpty property exactly for this purpose.
To check whether a collection is empty, use its isEmpty property
instead of comparing count to zero. Unless the collection guarantees
random-access performance, calculating count can be an O(n) operation.
extension RangeReplaceableCollection where Element == GetModel {
func matching(_ text: String?) -> Self {
guard let text = text, !text.isEmpty else { return self }
return filter { $0.data.contains { $0.name.localizedStandardContains(text) } }
}
}
edit/update:
If you need to filter your GetModal data:
extension RangeReplaceableCollection where Element == GetModel, Self: MutableCollection {
func matching(_ text: String?) -> Self {
guard let text = text, !text.isEmpty else { return self }
var collection = self
for index in collection.indices {
collection[index].data.removeAll { !$0.name.localizedStandardContains(text) }
}
collection.removeAll(where: \.data.isEmpty)
return collection
}
}

SwiftUI: List, ForEach, indices and .onDelete not working when using TextField() - (Index out of range) [duplicate]

Environment
Xcode 11.2.1 (11B500)
Problem
In order to implement editable teble with TextField on SwiftUI, I used ForEach(0..<items.count) to handle index.
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐢", "🐱"]
var body: some View {
List {
EditButton()
ForEach(0..<animals.count) { i in
TextField("", text: self.$animals[i])
}
}
}
}
However, problems arise if the table is changed to be deleteable.
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐢", "🐱"]
var body: some View {
List {
EditButton()
ForEach(0..<animals.count) { i in
TextField("", text: self.$animals[i]) // Thread 1: Fatal error: Index out of range
}
.onDelete { indexSet in
self.animals.remove(atOffsets: indexSet) // Delete "🐢" from animals
}
}
}
}
Thread 1: Fatal error: Index out of range when delete item
🐢 has been removed from animals and the ForEach loop seems to be running twice, even though animals.count is 1.
(lldb) po animals.count
1
(lldb) po animals
β–Ώ 1 element
- 0 : "🐱"
Please give me advice on the combination of Foreach and TextField.
Thanks.
Ok, the reason is in documentation for used ForEach constructor (as you see range is constant, so ForEach grabs initial range and holds it):
/// Creates an instance that computes views on demand over a *constant*
/// range.
///
/// This instance only reads the initial value of `data` and so it does not
/// need to identify views across updates.
///
/// To compute views on demand over a dynamic range use
/// `ForEach(_:id:content:)`.
public init(_ data: Range<Int>, #ViewBuilder content: #escaping (Int) -> Content)
So the solution would be to provide dynamic container. Below you can find a demo of possible approach.
Full module code
import SwiftUI
struct DummyView: View {
#State var animals: [String] = ["🐢", "🐱"]
var body: some View {
VStack {
HStack {
EditButton()
Button(action: { self.animals.append("Animal \(self.animals.count + 1)") }, label: {Text("Add")})
}
List {
ForEach(animals, id: \.self) { item in
EditorView(container: self.$animals, index: self.animals.firstIndex(of: item)!, text: item)
}
.onDelete { indexSet in
self.animals.remove(atOffsets: indexSet) // Delete "🐢" from animals
}
}
}
}
}
struct EditorView : 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
})
}
}
it is because editbutton is IN your list. place it ouside or better in navigationbar.

Resources