How do you use .enumerated() with ForEach in SwiftUI?
TL;DR
Warning: If you get in the habit of using enumerated()
with ForEach
, you may one day end up with EXC_BAD_INSTRUCTION
or Fatal error: Index out of bounds
exceptions. This is because not all collections have 0-based indexes.
A better default is to use zip
instead:
ForEach(Array(zip(items.indices, items)), id: \.0) { index, item in
// index and item are both safe to use here
}
The folks over at Point-Free mentioned that it's not safe to rely on enumerated()
with ForEach
in production since not all collections are zero-index based:
This is technically not the most correct way to do this. It would be more correct, and more verbose, to zip the
todos
array with its indices collection. In this case we are safe because we are dealing with a simple 0-based index array, but if we were doing this in production we should probablyzip
-based approach.
Apple's documentation for the enumerated function mentions this as well:
/// Returns a sequence of pairs (*n*, *x*), where *n* represents a
/// consecutive integer starting at zero and *x* represents an element of
/// the sequence.
///
/// This example enumerates the characters of the string "Swift" and prints
/// each character along with its place in the string.
///
/// for (n, c) in "Swift".enumerated() {
/// print("\(n): '\(c)'")
/// }
/// // Prints "0: 'S'"
/// // Prints "1: 'w'"
/// // Prints "2: 'i'"
/// // Prints "3: 'f'"
/// // Prints "4: 't'"
///
/// When you enumerate a collection, the integer part of each pair is a counter
/// for the enumeration, but is not necessarily the index of the paired value.
/// These counters can be used as indices only in instances of zero-based,
/// integer-indexed collections, such as `Array` and `ContiguousArray`. For
/// other collections the counters may be out of range or of the wrong type
/// to use as an index. To iterate over the elements of a collection with its
/// indices, use the `zip(_:_:)` function.
///
/// This example iterates over the indices and elements of a set, building a
/// list consisting of indices of names with five or fewer letters.
///
/// let names: Set = ["Sofia", "Camilla", "Martina", "Mateo", "Nicolás"]
/// var shorterIndices: [Set<String>.Index] = []
/// for (i, name) in zip(names.indices, names) {
/// if name.count <= 5 {
/// shorterIndices.append(i)
/// }
/// }
///
/// Now that the `shorterIndices` array holds the indices of the shorter
/// names in the `names` set, you can use those indices to access elements in
/// the set.
///
/// for i in shorterIndices {
/// print(names[i])
/// }
/// // Prints "Sofia"
/// // Prints "Mateo"
///
/// - Returns: A sequence of pairs enumerating the sequence.
///
/// - Complexity: O(1)
In your specific case enumerated()
is fine to use since you are using a 0-based index array, however due to the details above, relying on enumerated()
all the time can lead to non-obvious errors.
Take this snippet, for example:
ForEach(Array(items.enumerated()), id: \.offset) { offset, item in
Button(item, action: { store.didTapItem(at: offset) })
}
// ...
class Store {
var items: ArraySlice<String>
func didTapItem(at index: Int) {
print(items[index])
}
}
First notice that we dodged a bullet with Button(item...
since enumerated()
has guaranteed that item
can be accessed directly without causing an exception. However, if instead of item
we used items[offset]
, an exception could easily be raised.
Finally, the line print(items[index])
can easily lead to an exception since the index (really the offset) can be out of bounds.
Therefore, a safer approach is to always use the zip
method mentioned at the top of this post.
Another reason to prefer zip
is that if you tried using the same code with a different Collection (e.g. Set) you could get the following syntax error when indexing into the type (items[index]
):
Cannot convert value of type 'Int' to expected argument type 'Set.Index'
By using the zip
based approach, you can still index into the collection.
You could also create an extension on collection if you plan on using it often.
You can test this all out in a Playground:
import PlaygroundSupport
import SwiftUI
// MARK: - Array
let array = ["a", "b", "c"]
Array(array.enumerated()) // [(offset 0, element "a"), (offset 1, element "b"), (offset 2, element "c")]
Array(zip(array.indices, array)) // [(.0 0, .1 "a"), (.0 1, .1 "b"), (.0 2, .1 "c")]
let arrayView = Group {
ForEach(Array(array.enumerated()), id: \.offset) { offset, element in
PrintView("offset: \(offset), element: \(element)")
Text("value: \(array[offset])")
}
// offset: 0, element: a
// offset: 1, element: b
// offset: 2, element: c
ForEach(Array(zip(array.indices, array)), id: \.0) { index, element in
PrintView("index: \(index), element: \(element)")
Text("value: \(array[index])")
}
// index: 0, element: a
// index: 1, element: b
// index: 2, element: c
}
// MARK: - Array Slice
let arraySlice = array[1...2] // ["b", "c"]
Array(arraySlice.enumerated()) // [(offset 0, element "b"), (offset 1, element "c")]
Array(zip(arraySlice.indices, arraySlice)) // [(.0 1, .1 "b"), (.0 2, .1 "c")]
// arraySlice[0] // ❌ EXC_BAD_INSTRUCTION
arraySlice[1] // "b"
arraySlice[2] // "c"
let arraySliceView = Group {
ForEach(Array(arraySlice.enumerated()), id: \.offset) { offset, element in
PrintView("offset: \(offset), element: \(element)")
// Text("value: \(arraySlice[offset])") ❌ Fatal error: Index out of bounds
}
// offset: 0, element: b
// offset: 1, element: c
ForEach(Array(zip(arraySlice.indices, arraySlice)), id: \.0) { index, element in
PrintView("index: \(index), element: \(element)")
Text("value: \(arraySlice[index])")
}
// index: 1, element: b
// index: 2, element: c
}
// MARK: - Set
let set: Set = ["a", "b", "c"]
Array(set.enumerated()) // [(offset 0, element "b"), (offset 1, element "c"), (offset 2, element "a")]
Array(zip(set.indices, set)) // [({…}, .1 "a"), ({…}, .1 "b"), ({…}, .1 "c")]
let setView = Group {
ForEach(Array(set.enumerated()), id: \.offset) { offset, element in
PrintView("offset: \(offset), element: \(element)")
// Text("value: \(set[offset])") // ❌ Syntax error: Cannot convert value of type 'Int' to expected argument type 'Set<String>.Index'
}
// offset: 0, element: a
// offset: 1, element: b
// offset: 2, element: c
ForEach(Array(zip(set.indices, set)), id: \.0) { index, element in
PrintView("index: \(index), element: \(element)")
Text("value: \(set[index])")
}
// index: Index(_variant: Swift.Set<Swift.String>.Index._Variant.native(Swift._HashTable.Index(bucket: Swift._HashTable.Bucket(offset: 0), age: -481854246))), element: a
// index: Index(_variant: Swift.Set<Swift.String>.Index._Variant.native(Swift._HashTable.Index(bucket: Swift._HashTable.Bucket(offset: 2), age: -481854246))), element: b
// index: Index(_variant: Swift.Set<Swift.String>.Index._Variant.native(Swift._HashTable.Index(bucket: Swift._HashTable.Bucket(offset: 3), age: -481854246))), element: c
}
// MARK: -
struct PrintView: View {
init(_ string: String) {
print(string)
self.string = string
}
var string: String
var body: some View {
Text(string)
}
}
let allViews = Group {
arrayView
arraySliceView
setView
}
PlaygroundPage.current.setLiveView(allViews)
Updates:
- Deleted the part that mentioned you can use
\.1
since Peacemoon points out this could cause problems. Also I'm pretty sure if your items conform toIdentifiable
, there's no point in using zip in the first place, you should be able to just doForEach(identifiableItems)
.
Using enumerated with ForEach in SwiftUI
Let's assume we have an Array
of objects of type Item
:
struct Item {
let customID: Int
let value: String
}
let arrayNew = [
Item(customID: 1, value: "1"),
Item(customID: 23, value: "12"),
Item(customID: 2, value: "32")
]
Now, if we want to access both offset
and item
from the array, we need to use enumerated()
:
arrayNew.enumerated()
However, it returns an EnumeratedSequence
(and not an Array
):
@inlinable public func enumerated() -> EnumeratedSequence<Array<Element>>
If we take a look at the signature of ForEach
, we can see that it expects RandomAccessCollection
:
public struct ForEach<Data, ID, Content> where Data : RandomAccessCollection, ID : Hashable
The problem here is that EnumeratedSequence
doesn't conform to RandomAccessCollection
.
But Array
does - we just need to convert the result of enumerated()
back to an Array
:
Array(arrayNew.enumerated())
Now, we can use it directly in the ForEach
:
ForEach(Array(arrayNew.enumerated()), id: \.element.customID) { offset, item in
Text("\(offset) \(item.customID) \(item.value)")
}
SwiftUI Dynamically Create Enum or
You're making it much more complicated than necessary. You don't need an enum
, you can use any value, even your array values directly.
struct ContentView: View {
@AccessibilityFocusState var pickerAccessFocus: SalutationOptions?
@State private var salutation = ""
var salutationList: [SalutationOptions] = []
var body: some View {
List {
Section {
ForEach(salutationList, id: \.id) { salutation in
HStack {
Text(salutation.salutation)
} // End HStack
.contentShape(Rectangle())
.accessibilityFocused($pickerAccessFocus, equals: salutation)
} // End ForEach
} // End Section
} // End List
}
}
How do you use enumerated in a list of structs in ForEach Swiftui?
Here is fixed part
ForEach(Array(someData.buttonObjects.enumerated()), id: \.element.id) { ind, object in
HStack{
Text("\(ind)")
Text(object.name)
}
}
display index of each item in forEach loop in swift ui
For Swift Use .enumerated() with array to get index with item in for loop
let array = ["element1", "element2", "element3", "element4"]
for (index,item) in array.enumerated() {
print(index,item)
}
for SwiftUI this link will help you
Get index in ForEach in SwiftUI
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) }
}
}
}
how can i sort a Array(array.enumerated())
try this:
ForEach(Array(array.sorted(by: {$0.date ?? Date() < $1.date ?? Date()}).enumerated()),
id: \.element) { (index,item) in
}
SwiftUI get next item in ForEach loop
you could try adding a id
to your struct Item
, such as:
struct Item: Identifiable, Decodable, Hashable {
let id = UUID()
let card: Card
}
and then use:
VStack {
ForEach(item.cards, id: \.self) { theItem in
switch theItem.card.size {
case .Large:
LargeCard(card: theItem.card, viewModel: CardViewModel())
case .Medium:
MediumCard(card: theItem.card, viewModel: CardViewModel())
case .Small:
HStack(spacing: 16) {
SmallCard(card: theItem.card, viewModel: CardViewModel())
// here use the id to find the next item
if let ndx = item.cards.firstIndex(where: {$0.id == theItem.id}) {
if ndx + 1 < item.cards.count {
let nextItem = item.cards[ndx + 1]
if nextItem.card.size == .Small {
SmallCard(card: nextItem.card, viewModel: CardViewModel())
}
}
}
}
case .none:
Text("No more.")
}
}
}
You could also use enumerated as mentioned in the comments, such as:
VStack {
ForEach(Array(item.cards.enumerated()), id: \.offset) { index, theItem in
switch theItem.card.size {
case .Large:
LargeCard(card: theItem.card, viewModel: CardViewModel())
case .Medium:
MediumCard(card: theItem.card, viewModel: CardViewModel())
case .Small:
HStack(spacing: 16) {
SmallCard(card: theItem.card, viewModel: CardViewModel())
// here use the index to find the next item
if index + 1 < item.cards.count {
let nextItem = item.cards[index + 1]
if nextItem.card.size == .Small {
SmallCard(card: nextItem.card, viewModel: CardViewModel())
}
}
}
case .none:
Text("No more.")
}
}
}
Note, it looks like you should be using .environmentObject(viewModel)
to pass
a single CardViewModel()
to the views, instead of creating a new CardViewModel()
each time.
Related Topics
Get Button Pressed Id on Swift Via Sender
Get the Top Viewcontroller in iOS Swift
How to Transpose an Array More Swiftly
Open Uidatepicker Programmatically in iOS 14
How to Include .Swift File from Other .Swift File in an Immediate Mode
Using Function Parameter Names in Swift
Swift Setter Causing Exc_Bad_Access
Swift Custom Context Menu Previewprovider Can Not Click Any View Inside(Using Tapgesture)
Swift Pointer Problems with MACh_Task_Basic_Info
How to Convert Uicolor to Swiftui's Color
Ios13 Navigation Bar Large Titles Not Covering Status Bar
Break a Number Up to an Array of Individual Digits
Download File from Server Using Swift