У меня есть массив, и я хочу выполнить итерацию по нему, инициализировать представления на основе значения массива и хочу выполнить действие на основе индекса элемента массива.
Когда я перебираю объекты
ForEach(array, id: \.self) { item in
CustomView(item: item)
.tapAction {
self.doSomething(index) // Can't get index, so this won't work
}
}
Итак, я попробовал другой подход
ForEach((0..<array.count)) { index in
CustomView(item: array[index])
.tapAction {
self.doSomething(index)
}
}
Но проблема со вторым подходом заключается в том, что когда я меняю массив, например, если doSomething делает следующее
self.array = [1,2,3]
просмотры в ForEach не меняются, даже если значения меняются. Я считаю, что это происходит потому, что array.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) }
}
}
}
Свойство indices представляет собой диапазон чисел.
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) }
}
}
}
Хорошо, я обновил свой ответ. Если массив представляет собой @State, у меня отлично работает второй подход.
Никогда не используйте 0..<array.count. Вместо этого используйте array.indices. github.com/amomchilov/Блог/blob/мастер/…
В моем случае он делает это! Вы на бета 4? Симулятор или превью?
Обратите внимание, что примечания к выпуску iOS 13 beta 5 выдает следующее предупреждение о передаче диапазона в ForEach: «Однако вы не должны передавать диапазон, который изменяется во время выполнения. Если вы используете переменную, которая изменяется во время выполнения, для определения диапазона, в списке отображаются представления в соответствии с начальным диапазоном и игнорируются любые последующие обновления диапазона».
Спасибо, Роб, что объясняет ошибку индекса массива за пределами границ, сгенерированную вышеизложенным - содержимое моего массива со временем меняется. Проблематично, так как это по существу делает недействительным это решение для меня.
@TheNeil Я вижу ту же проблему, когда хочу отображать представление на основе индекса и хочу понять, почему это происходит? также какое решение вы придумали? заранее спасибо
У меня действительно нет решения, иначе я бы дал свой собственный ответ на этот вопрос. Привязка прокси, как показано ниже, выглядит как возможный обходной путь, но он запутан и далек от идеала. stackoverflow.com/a/61687192/2272431
Мне нужно было более универсальное решение, которое могло бы работать со всеми типами данных (которые реализуют RandomAccessCollection), а также предотвращать неопределенное поведение с помощью диапазонов.
Я закончил со следующим:
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)
}
}
Таким образом, исходный код в вопросе можно просто заменить на:
ForEachWithIndex(array, id: \.self) { index, item in
CustomView(item: item)
.tapAction {
self.doSomething(index) // Now works
}
}
Чтобы получить индекс, а также элемент.
Note that the API is mirrored to that of SwiftUI - this means that the initializer with the
idparameter'scontentclosure is not a@ViewBuilder.
The only change from that is theidparameter is visible and can be changed
Это замечательно. Одно небольшое предложение: если вы делаете данные общедоступными, вы также можете соответствовать DynamicViewContent (бесплатно), что позволяет работать модификаторам, таким как .onDelete().
@ConfusedVorlon о, действительно! Я обновил ответ, чтобы сделать data и content общедоступными, как в случае с исходным ForEach (я оставил id как есть), и добавил соответствие DynamicViewContent. Спасибо за предложение!
Спасибо за обновления. Не уверен, что это важно, но ForEach немного ограничивает соответствие расширению ForEach: DynamicViewContent, где Content: View {}
@ConfusedVorlon Я тоже не уверен, трудно сказать, не зная внутренней работы здесь. Добавлю на всякий случай; Я не думаю, что кто-то будет использовать это без View как Content ха-ха
Небольшой нюанс: data, content и id можно объявлять как константы.
Преимущество следующего подхода в том, что представления в ForEach меняются даже при изменении значений состояния:
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()
}
}
}
}
}
Другой подход заключается в использовании:
ForEach(Array(array.enumerated()), id: \.offset) { index, element in
// ...
}
Источник: https://alejandromp.com/blog/swiftui-enumerated/
Я думаю, вы хотите \.element убедиться, что анимация работает правильно.
ForEach(Array(array.enumerated()), id: \.1) {индекс, элемент в...}
что означает \.1?
@Luca Array(array.enumerated()) на самом деле имеет тип EnumeratedSequence<[ItemType]>, а его элемент является кортежем типа (offset: Int, element: ItemType). Написание \.1 означает, что вы обращаетесь к 1-му (в человеческом языке - 2-му) элементу кортежа, а именно - element: ItemType. Это то же самое, что написать \.element, но короче и, по-моему, гораздо менее понятно.
Использование \.element имеет дополнительное преимущество, заключающееся в том, что swiftUI может определить, когда содержимое массива изменилось, даже если размер не изменился. Используйте \.элемент!
Да, используйте \.element, как упоминал @ramzesenok, и реализуйте протокол Hashable, если вы используете настраиваемый массив объектов, и добавьте свойства, которые могут измениться/обновиться внутри вашего объекта внутри func hash(into hasher: inout Hasher) {, и добавьте, например, в моем случае обновление статуса, например status.hash(into: &hasher)
Для этой цели я создал специальный View на основе замечательного решения Stone:
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(self.data.enumerated()), id: \.offset) { idx, item in
self.content(idx, item)
}
}
}
Теперь вы можете использовать его так:
EnumeratedForEach(items) { idx, item in
...
}
Вот простое решение, хотя и довольно неэффективное для приведенных выше.
В своем Tap Action пройдите через свой предмет
.tapAction {
var index = self.getPosition(item)
}
Затем создайте функцию, которая находит индекс этого элемента, сравнивая идентификатор
func getPosition(item: Item) -> Int {
for i in 0..<array.count {
if (array[i].id == item.id){
return i
}
}
return 0
}
Для ненулевых массивов избегайте использования enumerated, вместо этого используйте zip:
ForEach(Array(zip(items.indices, items)), id: \.0) { index, item in
// Add Code here
}
Из любопытства, почему бы вам не использовать массив с отсчетом от нуля? Каков контекст?
Например, индексы ArraySlice будут индексами его элементов в его родительском массиве, поэтому они не могут начинаться с 0.
Решение 2021, если вы используете ненулевые массивы, избегайте перечисления:
ForEach(array.indices,id:\.self) { index in
VStack {
Text(array[index].name)
.customFont(name: "STC", style: .headline)
.foregroundColor(Color.themeTitle)
}
}
}
Обычно я использую enumerated, чтобы получить пару index и element с element вместо id.
ForEach(Array(array.enumerated()), id: \.element) { index, element in
Text("\(index)")
Text(element.description)
}
Для более многократного использования компонента вы можете посетить эту статью https://onmyway133.com/posts/how-to-use-foreach-with-indices-in-swiftui/
Полезно, красиво сделано!
ForEach is SwiftUI — это не то же самое, что цикл for, он на самом деле создает нечто, называемое структурной идентичностью. В документации ForEach говорится:
/// 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.
Это означает, что мы не можем использовать индексы, перечисления или новый массив в ForEach. ForEach должен находиться в фактическом массиве идентифицируемых элементов. Это делается для того, чтобы SwiftUI мог отслеживать перемещение представлений строк.
Чтобы решить вашу проблему с получением индекса, вам просто нужно найти индекс следующим образом:
ForEach(items) { item in
CustomView(item: item)
.tapAction {
if let index = array.firstIndex(where: { $0.id == item.id }) {
self.doSomething(index)
}
}
}
Вы можете увидеть, как Apple делает это в своем Учебник по образцу приложения Scrumdinger.
guard let scrumIndex = scrums.firstIndex(where: { $0.id == scrum.id }) else {
fatalError("Can't find scrum in array")
}
это приведет к вялости, когда у вас много строк.
Хотя это работает, я обнаружил, что это увеличивает ваши шансы столкнуться с ужасной ошибкой «компилятор не может проверить тип этого выражения за разумное время». У onmyway133 есть отличный ответ ниже о том, как это сделать хорошо
если вы не используете идентификаторы элементов, вы не можете изменить массив
Как они уже упоминали, вы можете использовать array.indices для этой цели.
НО помните, что индексы, которые у вас есть, начинаются с последнего элемента массива. Чтобы решить эту проблему, вы должны использовать это: array.indices.reversed() также вы должны указать идентификатор для ForEach.
Вот пример:
ForEach(array.indices.reversed(), id:\.self) { index in }
Я считаю, что
array.firstIndex(of: item)не будет работать, если в массиве есть одинаковые элементы, например, если массив[1,1,1], он вернет0для всех элементов. Кроме того, это не очень хорошая производительность для каждого элемента массива. мой массив является переменной@State.@State private var array: [Int] = [1, 1, 2]