SwiftUI's ForEach crashes when array's element is removed - swift

I try to lay out my UI elements in rows - two elements in a row, for that I use two ForEach, one for rows and one for elements in a row. Each UI element has #Binding so I pass a structure from my model's array. I should be able to add or remove the elements dynamically and everything works except one thing - when I remove the only element from a row my app crashes with error Fatal error: Index out of range: file Swift/ContiguousArrayBuffer.swift, line 444. I have read several topics on SO but I haven't found an answer.
This is how my code looks like:
struct PickerElement: Hashable {
let id: Int
let title: String
let value: Int
}
struct CellModel: Hashable {
var element: PickerElement?
var error: String?
}
struct PickerButton: View {
#Binding var value: CellModel?
#Binding var error: String?
(...)
}
class MyModel: ObservableObject {
#Published var counter = 0
#Published var cellModels = [CellModel]()
private var cancellables = Set<AnyCancellable>()
$counter.sink { value in
let diff = value - self.cellModels.count
if diff > 0 {
self.cellModels.append(contentsOf:
Array(repeating: CellModel(), count: diff)
)
} else if diff < 0 {
self.cellModels = Array(
self.cellModels.prefix(value)
)
}
}.store(in: &cancellables)
}
struct MyView: View {
#ObservedObject var model: MyModel
var body: some View {
VStack(spacing: 8) {
layOutElements()
}
}
#ViewBuilder
func layOutElements() -> some View {
let elementsCount = model.cellModels.count
if elementsCount > 0 {
VStack {
HStack {
Spacer()
Text("Some title").font(.caption)
Spacer()
}.padding()
// count number of rows
let rowsCount = Int(ceil(Double(elementsCount) / 2.0))
// lay out rows
ForEach(0 ..< rowsCount, id: \.self) { rowIndex in
layOutRow(rowIndex: rowIndex,
elementsCount: elementsCount,
rowsCount: rowsCount)
}
}
}
}
#ViewBuilder
private func layOutRow(rowIndex: Int, elementsCount: Int, rowsCount: Int) -> some View {
HStack(alignment: .top, spacing: 8) {
let firstCellInRowIndex = rowIndex * 2
let lastCellInRowIndex = min(elementsCount - 1, firstCellInRowIndex + 1)
ForEach(firstCellInRowIndex ... lastCellInRowIndex, id: \.self) { elementIndex in
PickerButton(value: $model.cellModels[elementIndex].element, // <--- *1
error: $model.cellModels[elementIndex].error) // <--- *2
}
// *1 , *2 - if I changed the lines and pass dummy bindings (not array's elements) there, the code would work without any glitches
if rowIndex == rowsCount - 1 && !elementsCount.isMultiple(of: 2) {
Spacer()
.frame(minWidth: 0, maxWidth: .infinity)
}
}
}
If I change a value of #Published var counter = 0 everything works properly, views are added and removed, but while decrementing if SwiftUI tries to remove the last remaining element from a row the app crashes. As I have commented in the code above, if I don't bind PickerButton to the structures from my model's array, the app doesn't crash. How to fix this issue? (I need to use indexes because I have to count rows and cells in a row)

The crash happens when elementsCount in layOutElements() becomes 0. Then layOutElements() doesn't return a View, which should be impossible. You can add an else to your if statement that returns EmptyView() or some placeholder.
Other than that, in my experience ForEach with dynamic ranges is a mess. If you need indexes you can use Array.enumerated() which has given me better results.

Related

SwiftUI - adding an extra row in a list every 4 rows

In my code I display numbers in a list. User can choose the grouping method and the numbers will be put into sections accordingly (either groups of 5 elements or odd/even). Now, I would like to add a green row after 4*n elements where n=1,2,3,.. as seen from the UI perspective (not the perspective of the data source !). So after the fourth row, a green row should follow. After eighth row, a green row should follow etc.
In my current code this works for the groups of 5 elements but does not work for the odd/even variant. The problem seems to be in the indexes because they don't depend on the actual placement.
I know this seems to sound a bit complicated but maybe someone has an idea how to approach the problem, ideally in some scalable way so that if I add a third grouping method in the future it will all also work.
import SwiftUI
import Combine
struct ContentView: View {
#StateObject var myViewModel = MyViewModel()
var body: some View {
VStack {
Button {
myViewModel.groupStrategy.send(myViewModel.groupStrategy.value == .multiplesOfFive ? .oddEven : .multiplesOfFive)
} label: {
Text("Toggle grouping strategy")
}
List() {
ForEach(myViewModel.numOfSections, id:\.self) { sectNum in
Section("Sc \(sectNum)") {
ForEach(Array(myViewModel.nums.enumerated()), id: \.offset) { idx, element in
let _ = print("Sc \(sectNum) \(idx) \(element)")
if myViewModel.shouldSkipNumberInThisSection(number: element, sectionNumber: sectNum) {
EmptyView()
} else {
Text(element.description + " idx: " + idx.description)
if idx > 0 && (idx+1) % 4 == 0 {
Color.green
}
}
}
}
}
}
}
.padding()
}
}
class MyViewModel: ObservableObject {
enum GroupStrategy {
case multiplesOfFive
case oddEven
}
#Published var nums: [Int]
#Published var numOfSections: [Int] = []
var groupStrategy = CurrentValueSubject<GroupStrategy, Never>(.multiplesOfFive)
private var cancellables: Set<AnyCancellable> = []
func shouldSkipNumberInThisSection(number: Int, sectionNumber: Int) -> Bool {
switch groupStrategy.value {
case .multiplesOfFive:
return number >= sectionNumber * 5 || number < (sectionNumber-1) * 5
case .oddEven:
return sectionNumber == 0 ? (number % 2) == 0 : (number % 2) != 0
}
}
func shouldPutGreenRow() -> Bool {
return false
}
init() {
self.nums = []
let numbers: [Int] = Array(3...27)
self.nums = numbers
self.numOfSections = Array(1..<Int(nums.count / 5)+1)
groupStrategy.sink { strategy in
switch self.groupStrategy.value {
case .multiplesOfFive:
self.numOfSections = Array(1..<Int(self.nums.count / 5)+1)
case .oddEven:
self.numOfSections = Array(0..<2)
}
}.store(in: &cancellables)
}
}
For the multiplesOfFive group - OK:
For the odd/even group - NOT OK:
In the odd/even group, the green row should appear after numbers 9, 17, 25, 8, 16, 24. Instead, it appears only in the group of even numbers
The code is not working for the odd/even section because, for each section, you are iterating over all numbers and re-creating the indexes (or idx in your code).
So, the condition if idx > 0 && (idx+1) % 4 == 0 is good for the multiples of five, instead for odd/even it should be if idx > 0 && (idx + sectNum + 2) % 8 == 0. However, if you just add this condition to the view, it gets less scalable, as you will need to create new conditions for each different grouping.
I have thought of an alternative to your View Model using a dictionary within a dictionary, to store the sections, the indexes and the values, with no repeated entries.
With this approach, the view model gets a little bit more complicated, but the underlying data is the same, and the view is scalable: if you create a new GroupingStrategy, you don't need to change the view.
Here's the code with comments, I hope it might help you:
A bold view model:
class MyViewModel: ObservableObject {
// Create raw values and conform to CaseIterable,
// so you don't need to change the view when creating new strategies
enum GroupStrategy: String, CaseIterable {
case multiplesOfFive = "Groups of 5 items"
case oddEven = "Odd vs. even"
}
// This is the underlying data: no changes
#Published var nums = Array(3...27)
// This is where the sections are stored: a dictionary that
// includes the numbers within and their position in the final sequence.
#Published var sequence = [Int:[Int:Int]]()
// It can work with any type of underlying data, not just Int.
// For example, if the data is of type MyModel, this variable can be of type
// [Int:[Int:MyModel]]
#Published var groupStrategy = GroupStrategy.multiplesOfFive {
didSet {
// Will rebuild the dictionary every time the grouping changes
rebuildGroupStrategy()
}
}
// Extract all the sections for the current strategy
var sections: [Int] {
Array(Set(sequence.keys)).sorted()
}
// Extract all the numbers for a specific section: the key of
// the dictionary is the index in the sequence
func numsInSection(_ section: Int) -> [Int:Int] {
return sequence[section] ?? [:]
}
// Define the strategies in the Enum, then here...
var arrayOfSections: [Int] {
switch groupStrategy {
case .multiplesOfFive:
return Array(1..<Int(nums.count / 5) + 1)
case .oddEven:
return [0, 1]
}
}
// ... and here
func isIncludedInSection(number: Int, section: Int) -> Bool {
switch groupStrategy {
case .multiplesOfFive:
return number < section * 5 && number >= (section - 1) * 5
case .oddEven:
return section == 0 ? (number % 2) != 0 : (number % 2) == 0
}
}
// When you need to set a new strategy
func rebuildGroupStrategy() {
sequence = [:]
var prog = 0
// Create the sequence, which will not contain repeated elements
arrayOfSections.forEach { section in
sequence[section] = [:]
nums.forEach { number in
if isIncludedInSection(number: number, section: section) {
sequence[section]?[prog] = number
prog += 1
}
}
}
}
init() {
rebuildGroupStrategy()
}
}
A simpler view:
struct MyView: View {
#StateObject var myViewModel = MyViewModel()
var body: some View {
VStack {
// Use a Picker for scalability
Picker("Group by:", selection: $myViewModel.groupStrategy) {
ForEach(MyViewModel.GroupStrategy.allCases, id: \.self) { item in
Text(item.rawValue)
}
}
List() {
// Iterate over the sections
ForEach(myViewModel.sections, id:\.self) { sectNum in
Section("Sc \(sectNum)") {
let numbers = myViewModel.numsInSection(sectNum)
// Iterate only over the numbers of each section
ForEach(numbers.keys.sorted(), id: \.self) { index in
Text("\(numbers[index] ?? 0), Index = \(index)")
// Same condition ever: paint it green after every 4th row
if (index + 1) % 4 == 0 {
Color.green
}
}
}
}
}
}
.padding()
}
}

SwiftUI memory leak

I'm getting a weird memory leak in SwiftUI when using List and id: \.self, where only some of the items are destroyed. I'm using macOS Monterey Beta 5.
Here is how to reproduce:
Create a new blank SwiftUI macOS project
Paste the following code:
class Model: ObservableObject {
#Published var objs = (1..<100).map { TestObj(text: "\($0)")}
}
class TestObj: Hashable {
let text: String
static var numDestroyed = 0
init(text: String) {
self.text = text
}
static func == (lhs: TestObj, rhs: TestObj) -> Bool {
return lhs.text == rhs.text
}
func hash(into hasher: inout Hasher) {
hasher.combine(text)
}
deinit {
TestObj.numDestroyed += 1
print("Deinit: \(TestObj.numDestroyed)")
}
}
struct ContentView: View {
#StateObject var model = Model()
var body: some View {
NavigationView {
List(model.objs, id: \.self) { obj in
Text(obj.text)
}
Button(action: {
var i = 1
model.objs.removeAll(where: { _ in
i += 1
return i % 2 == 0
})
}) {
Text("Remove half")
}
}
}
}
Run the app, and press the "Remove half" button. Keep pressing it until all the items are gone. However, if you look at the console, you'll see that only 85 items have been destroyed, while there were 99 items. The Xcode memory graph also supports this.
This seems to be caused by the id: \.self line. Removing it and switching it out for id: \.text fixes the problem.
However the reason I use id: \.self is because I want to support multiple selection, and I want the selection to be of type Set<TestObj>, instead of Set<UUID>.
Is there any way to solve this issue?
If you didn't have to use selection in your List, you could use any unique & constant id, for example:
class TestObj: Hashable, Identifiable {
let id = UUID()
/* ... */
}
And then your List with the implicit id: \.id:
List(model.objs) { obj in
Text(obj.text)
}
This works great. It works because now you are no longer identifying the rows in the list by a reference type, which is kept by SwiftUI. Instead you are using a value type, so there aren't any strong references causing TestObjs to not deallocate.
But you need selection in List, so see more below about how to achieve that.
To get this working with selection, I will be using OrderedDictionary from Swift Collections. This is so the list rows can still be identified with id like above, but we can quickly access them. It's partially a dictionary, and partially an array, so it's O(1) time to access an element by a key.
Firstly, here is an extension to create this dictionary from the array, so we can identify it by its id:
extension OrderedDictionary {
/// Create an ordered dictionary from the given sequence, with the key of each pair specified by the key-path.
/// - Parameters:
/// - values: Every element to create the dictionary with.
/// - keyPath: Key-path for key.
init<Values: Sequence>(_ values: Values, key keyPath: KeyPath<Value, Key>) where Values.Element == Value {
self.init()
for value in values {
self[value[keyPath: keyPath]] = value
}
}
}
Change your Model object to this:
class Model: ObservableObject {
#Published var objs: OrderedDictionary<UUID, TestObj>
init() {
let values = (1..<100).map { TestObj(text: "\($0)")}
objs = OrderedDictionary<UUID, TestObj>(values, key: \.id)
}
}
And rather than model.objs you'll use model.objs.values, but that's it!
See full demo code below to test the selection:
struct ContentView: View {
#StateObject private var model = Model()
#State private var selection: Set<UUID> = []
var body: some View {
NavigationView {
VStack {
List(model.objs.values, selection: $selection) { obj in
Text(obj.text)
}
Button(action: {
var i = 1
model.objs.removeAll(where: { _ in
i += 1
return i % 2 == 0
})
}) {
Text("Remove half")
}
}
.onChange(of: selection) { newSelection in
let texts = newSelection.compactMap { selection in
model.objs[selection]?.text
}
print(texts)
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
EditButton()
}
}
}
}
}
Result:

How to increment a state in a ForEach loop in SwiftUI?

I need to increment the genNum variable and pass it to another struct in a ForEach loop. My code is compiling properly but cannot preview it in the simulation as well as the canvas. Getting "cannot preview in this file". Another error I am getting is "RemoteHumanReadableError: The operation couldn’t be completed. (BSServiceConnectionErrorDomain error 3.)".
BSServiceConnectionErrorDomain (3):
==BSErrorCodeDescription: OperationFailed
import SwiftUI
#available(iOS 14.0, *)
struct CategoryView: View {
#State var genNum: Int = -1
var categories: [Int: [PokemonData]] {
Dictionary(
grouping: pokemonData,
by: { $0.generation }
)
}
var columns: [GridItem] = [
GridItem(.fixed(170)),
GridItem(.fixed(170))
]
var body: some View {
NavigationView {
ScrollView {
LazyVGrid(columns: columns) {
ForEach(categories.keys.sorted(), id: \.self) { key in
CategoryCard(genNum: self.increment()) // <--- Having problem with this line
}
}
AllCard()
.padding(.horizontal, 15)
.padding(.bottom, 20)
}
.navigationTitle("Categories")
}
}
// Function to increment the state value
func increment() -> Int {
self.genNum += 1
let i = genNum
return i
}
}
If all you want to do is to pass values from 0 to count-1 to your CategoryCard() initializer, you can use the enumerated() function. That function takes an array as input and returns an array of tuples where the first item is an array index and the 2nd item is the the element from the original array.
This code, for example:
let array = ["zero", "one", "two", "three"]
array.enumerated().forEach { (index, string) in
print (index, string)
}
Outputs the following:
0 zero
1 one
2 two
3 three
So you could rewrite your code like this:
LazyVGrid(columns: columns) {
ForEach(categories.keys.sorted().enumerated(), id: \.self) { (index, key) in
CategoryCard(genNum: index) // <--- Having problem with this line
}
}
(Disclaimer: I haven't used SwiftUI yet, so I'm not totally clear on what ForEach is doing inside your LazyGrid)

SwiftUI core data, grouped list fetch result

using core data im storing some airport and for every airport i'm storing different note
I have created the entity Airport and the entity Briefing
Airport have 1 attribute called icaoAPT and Briefing have 4 attribute category, descript, icaoAPT, noteID
On my detailsView I show the list all the noted related to that airport, I managed to have a dynamic fetch via another view called FilterList
import SwiftUI
import CoreData
struct FilterLIst: View {
var fetchRequest: FetchRequest<Briefing>
#Environment(\.managedObjectContext) var dbContext
init(filter: String) {
fetchRequest = FetchRequest<Briefing>(entity: Briefing.entity(), sortDescriptors: [], predicate: NSPredicate(format: "airportRel.icaoAPT == %#", filter))
}
func update(_ result : FetchedResults<Briefing>) ->[[Briefing]]{
return Dictionary(grouping: result) { (sequence : Briefing) in
sequence.category
}.values.map{$0}
}
var body: some View {
List{
ForEach(update(self.fetchRequest.wrappedValue), id: \.self) { (section : Briefing) in
Section(header: Text(section.category!)) {
ForEach(section, id: \.self) { note in
Text("hello")
/// Xcode error Cannot convert value of type 'Text' to closure result type '_'
}
}
}
}
}
}
on this view I'm try to display all the section divided by category using the func update...
but Xcode give me this error , I can't understand why..Cannot convert value of type 'Text' to closure result type '_'
fore reference I list below my detailsView
import SwiftUI
struct DeatailsView: View {
#Environment(\.managedObjectContext) var dbContext
#Environment(\.presentationMode) var presentation
#State var airport : Airport
#State var note = ""
#State var noteTitle = ["SAFTY NOTE", "TAXI NOTE", "CPNOTE"]
#State var notaTitleSelected : Int = 0
#State var notaID = ""
var body: some View {
Form{
Section(header: Text("ADD NOTE Section")) {
TextField("notaID", text: self.$notaID)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
TextField("add Note descrip", text: self.$note)
.textFieldStyle(RoundedBorderTextFieldStyle())
.padding()
Picker(selection: $notaTitleSelected, label: Text("Class of Note")) {
ForEach(0 ..< noteTitle.count) {
Text(self.noteTitle[$0])
}
}
HStack{
Spacer()
Button(action: {
let nota = Briefing(context: self.dbContext)
nota.airportRel = self.airport
nota.icaoAPT = self.airport.icaoAPT
nota.descript = self.note
nota.category = self.noteTitle[self.notaTitleSelected]
nota.noteID = self.notaID
do {
try self.dbContext.save()
debugPrint("salvato notazione")
} catch {
print("errore nel salva")
}
}) {
Text("Salva NOTA")
}
Spacer()
}
}
Section(header: Text("View Note")) {
FilterLIst(filter: airport.icaoAPT ?? "NA")
}
}
}
}
thanks for the help
This is because you try to iterate over a single Briefing object and a ForEach loop expects a collection:
List {
ForEach(update(self.fetchRequest.wrappedValue), id: \.self) { (section: Briefing) in
Section(header: Text(section.category!)) {
ForEach(section, id: \.self) { note in // <- section is a single object
Text("hello")
/// Xcode error Cannot convert value of type 'Text' to closure result type '_'
}
}
}
}
I'd recommend you to extract the second ForEach to another method for clarity. This way you can also be sure you're passing the argument of right type ([Briefing]):
func categoryView(section: [Briefing]) -> some View {
ForEach(section, id: \.self) { briefing in
Text("hello")
}
}
Note that the result of your update method is of type [[Briefing]], which means the parameter in the ForEach is section: [Briefing] (and not Briefing):
var body: some View {
let data: [[Briefing]] = update(self.fetchRequest.wrappedValue)
return List {
ForEach(data, id: \.self) { (section: [Briefing]) in
Section(header: Text("")) { // <- can't be `section.category!`
self.categoryView(section: section)
}
}
}
}
This also means you can't write section.category! in the header as the section is an array.
You may need to access a Briefing object to get a category:
Text(section[0].category!)
(if you're sure the first element exists).
For clarity I specified types explicitly. It's also a good way to be sure you always use the right type.
let data: [[Briefing]] = update(self.fetchRequest.wrappedValue)
However, Swift can infer types automatically. In the example below, the data will be of type [[Briefing]]:
let data = update(self.fetchRequest.wrappedValue)

Get index in ForEach in SwiftUI

I have an array and I want to iterate through it initialize views based on array value, and want to perform action based on array item index
When I iterate through objects
ForEach(array, id: \.self) { item in
CustomView(item: item)
.tapAction {
self.doSomething(index) // Can't get index, so this won't work
}
}
So, I've tried another approach
ForEach((0..<array.count)) { index in
CustomView(item: array[index])
.tapAction {
self.doSomething(index)
}
}
But the issue with second approach is, that when I change array, for example, if doSomething does following
self.array = [1,2,3]
views in ForEach do not change, even if values are changed. I believe, that happens because array.count haven't changed.
Is there a solution for this?
Another approach is to use:
enumerated()
ForEach(Array(array.enumerated()), id: \.offset) { index, element in
// ...
}
Source: https://alejandromp.com/blog/swiftui-enumerated/
This works for me:
Using Range and Count
struct ContentView: View {
#State private var array = [1, 1, 2]
func doSomething(index: Int) {
self.array = [1, 2, 3]
}
var body: some View {
ForEach(0..<array.count) { i in
Text("\(self.array[i])")
.onTapGesture { self.doSomething(index: i) }
}
}
}
Using Array's Indices
The indices property is a range of numbers.
struct ContentView: View {
#State private var array = [1, 1, 2]
func doSomething(index: Int) {
self.array = [1, 2, 3]
}
var body: some View {
ForEach(array.indices) { i in
Text("\(self.array[i])")
.onTapGesture { self.doSomething(index: i) }
}
}
}
I usually use enumerated to get a pair of index and element with the element as the id
ForEach(Array(array.enumerated()), id: \.element) { index, element in
Text("\(index)")
Text(element.description)
}
For a more reusable component, you can visit this article https://onmyway133.com/posts/how-to-use-foreach-with-indices-in-swiftui/
I needed a more generic solution, that could work on all kind of data (that implements RandomAccessCollection), and also prevent undefined behavior by using ranges.
I ended up with the following:
public struct ForEachWithIndex<Data: RandomAccessCollection, ID: Hashable, Content: View>: View {
public var data: Data
public var content: (_ index: Data.Index, _ element: Data.Element) -> Content
var id: KeyPath<Data.Element, ID>
public init(_ data: Data, id: KeyPath<Data.Element, ID>, content: #escaping (_ index: Data.Index, _ element: Data.Element) -> Content) {
self.data = data
self.id = id
self.content = content
}
public var body: some View {
ForEach(
zip(self.data.indices, self.data).map { index, element in
IndexInfo(
index: index,
id: self.id,
element: element
)
},
id: \.elementID
) { indexInfo in
self.content(indexInfo.index, indexInfo.element)
}
}
}
extension ForEachWithIndex where ID == Data.Element.ID, Content: View, Data.Element: Identifiable {
public init(_ data: Data, #ViewBuilder content: #escaping (_ index: Data.Index, _ element: Data.Element) -> Content) {
self.init(data, id: \.id, content: content)
}
}
extension ForEachWithIndex: DynamicViewContent where Content: View {
}
private struct IndexInfo<Index, Element, ID: Hashable>: Hashable {
let index: Index
let id: KeyPath<Element, ID>
let element: Element
var elementID: ID {
self.element[keyPath: self.id]
}
static func == (_ lhs: IndexInfo, _ rhs: IndexInfo) -> Bool {
lhs.elementID == rhs.elementID
}
func hash(into hasher: inout Hasher) {
self.elementID.hash(into: &hasher)
}
}
This way, the original code in the question can just be replaced by:
ForEachWithIndex(array, id: \.self) { index, item in
CustomView(item: item)
.tapAction {
self.doSomething(index) // Now works
}
}
To get the index as well as the element.
Note that the API is mirrored to that of SwiftUI - this means that the initializer with the id parameter's content closure is not a #ViewBuilder.
The only change from that is the id parameter is visible and can be changed
For non zero based arrays avoid using enumerated, instead use zip:
ForEach(Array(zip(items.indices, items)), id: \.0) { index, item in
// Add Code here
}
I created a dedicated View for this purpose:
struct EnumeratedForEach<ItemType, ContentView: View>: View {
let data: [ItemType]
let content: (Int, ItemType) -> ContentView
init(_ data: [ItemType], #ViewBuilder content: #escaping (Int, ItemType) -> ContentView) {
self.data = data
self.content = content
}
var body: some View {
ForEach(Array(zip(data.indices, data)), id: \.0) { idx, item in
content(idx, item)
}
}
}
Now you can use it like this:
EnumeratedForEach(items) { idx, item in
...
}
ForEach is SwiftUI isn’t the same as a for loop, it’s actually doing something called structural identity. 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 given the actual array of identifiable items. This is so SwiftUI can animate the rows around to match the data, obviously this can't work with indicies, e.g. if row at 0 is moved to 1 its index is still 0.
To solve your problem of getting the index, you simply have to look up the index like this:
ForEach(items) { item in
CustomView(item: item)
.tapAction {
if let index = array.firstIndex(where: { $0.id == item.id }) {
self.doSomething(index)
}
}
}
You can see Apple doing this in their Scrumdinger sample app tutorial.
guard let scrumIndex = scrums.firstIndex(where: { $0.id == scrum.id }) else {
fatalError("Can't find scrum in array")
}
The advantage of the following approach is that the views in ForEach even change if state values ​​change:
struct ContentView: View {
#State private var array = [1, 2, 3]
func doSomething(index: Int) {
self.array[index] = Int.random(in: 1..<100)
}
var body: some View {
let arrayIndexed = array.enumerated().map({ $0 })
return List(arrayIndexed, id: \.element) { index, item in
Text("\(item)")
.padding(20)
.background(Color.green)
.onTapGesture {
self.doSomething(index: index)
}
}
}
}
... this can also be used, for example, to remove the last divider
in a list:
struct ContentView: View {
init() {
UITableView.appearance().separatorStyle = .none
}
var body: some View {
let arrayIndexed = [Int](1...5).enumerated().map({ $0 })
return List(arrayIndexed, id: \.element) { index, number in
VStack(alignment: .leading) {
Text("\(number)")
if index < arrayIndexed.count - 1 {
Divider()
}
}
}
}
}
2021 solution if you use non zero based arrays avoid using enumerated:
ForEach(array.indices,id:\.self) { index in
VStack {
Text(array[index].name)
.customFont(name: "STC", style: .headline)
.foregroundColor(Color.themeTitle)
}
}
}
To get indexing from SwiftUI's ForEach loop, you could use closure's shorthand argument names:
#State private var cars = ["Aurus","Bentley","Cadillac","Genesis"]
var body: some View {
NavigationView {
List {
ForEach(Array(cars.enumerated()), id: \.offset) {
Text("\($0.element) at \($0.offset) index")
}
}
}
}
Results:
// Aurus at 0 index
// Bentley at 1 index
// Cadillac at 2 index
// Genesis at 3 index
P. S.
Initially, I posted an answer with a "common" expression that all Swift developers are used to, however, thanks to #loremipsum I changed it. As stated in WWDC 2021 Demystify SwiftUI video (time 33:40), array indices are not stable from \.self identity (key path).
ForEach(0 ..< cars.count, id: \.self) { // – NOT STABLE
Text("\(cars[$0]) at \($0) index")
}
Here is a simple solution though quite inefficient to the ones above..
In your Tap Action, pass through your item
.tapAction {
var index = self.getPosition(item)
}
Then create a function the finds the index of that item by comparing the id
func getPosition(item: Item) -> Int {
for i in 0..<array.count {
if (array[i].id == item.id){
return i
}
}
return 0
}
You can use this method:
.enumerated()
From the Swift documentation:
Returns a sequence of pairs (n, x), where n represents a consecutive
integer starting at zero and x represents an element of the sequence.
var elements: [String] = ["element 1", "element 2", "element 3", "element 4"]
ForEach(Array(elements.enumerated()), id: \.element) { index, element in
Text("\(index) \(element)")
}
Just like they mentioned you can use array.indices for this purpose
BUT remember that indexes that you've got are started from last element of array, To fix this issue you must use this: array.indices.reversed() also you should provide an id for the ForEach.
Here's an example:
ForEach(array.indices.reversed(), id:\.self) { index in }