Получить индекс в ForEach в SwiftUI

У меня есть массив, и я хочу выполнить итерацию по нему, инициализировать представления на основе значения массива и хочу выполнить действие на основе индекса элемента массива.

Когда я перебираю объекты

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 не изменились.

Есть ли решение для этого? Заранее спасибо.

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
111
0
72 785
11
Перейти к ответу Данный вопрос помечен как решенный

Ответы 11

Ответ принят как подходящий

Это работает для меня:

Использование диапазона и количества

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) }
        }
    }
}

Я считаю, что array.firstIndex(of: item) не будет работать, если в массиве есть одинаковые элементы, например, если массив [1,1,1], он вернет 0 для всех элементов. Кроме того, это не очень хорошая производительность для каждого элемента массива. мой массив является переменной @State. @State private var array: [Int] = [1, 1, 2]

Pavel 28.07.2019 23:02

Хорошо, я обновил свой ответ. Если массив представляет собой @State, у меня отлично работает второй подход.

kontiki 28.07.2019 23:26

Никогда не используйте 0..<array.count. Вместо этого используйте array.indices. github.com/amomchilov/Блог/blob/мастер/…

Alexander 28.07.2019 23:33

В моем случае он делает это! Вы на бета 4? Симулятор или превью?

kontiki 29.07.2019 09:59

Обратите внимание, что примечания к выпуску iOS 13 beta 5 выдает следующее предупреждение о передаче диапазона в ForEach: «Однако вы не должны передавать диапазон, который изменяется во время выполнения. Если вы используете переменную, которая изменяется во время выполнения, для определения диапазона, в списке отображаются представления в соответствии с начальным диапазоном и игнорируются любые последующие обновления диапазона».

rob mayoff 30.07.2019 01:12

Спасибо, Роб, что объясняет ошибку индекса массива за пределами границ, сгенерированную вышеизложенным - содержимое моего массива со временем меняется. Проблематично, так как это по существу делает недействительным это решение для меня.

TheNeil 18.05.2020 22:19

@TheNeil Я вижу ту же проблему, когда хочу отображать представление на основе индекса и хочу понять, почему это происходит? также какое решение вы придумали? заранее спасибо

Naren 07.06.2020 04:22

У меня действительно нет решения, иначе я бы дал свой собственный ответ на этот вопрос. Привязка прокси, как показано ниже, выглядит как возможный обходной путь, но он запутан и далек от идеала. stackoverflow.com/a/61687192/2272431

TheNeil 07.06.2020 05:59
developer.apple.com/videos/play/wwdc2021/10022 Использование индекса не идеально, если позиция содержимого массива может измениться, например, добавить или удалить содержимое. Поскольку индекс используется для неявной идентификации представлений. Мы должны явно установить модификатор id и использовать согласованный идентификатор для одного и того же представления.
Atif 09.07.2021 10:52

Мне нужно было более универсальное решение, которое могло бы работать со всеми типами данных (которые реализуют 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 id parameter's content closure is not a @ViewBuilder.
The only change from that is the id parameter is visible and can be changed

Это замечательно. Одно небольшое предложение: если вы делаете данные общедоступными, вы также можете соответствовать DynamicViewContent (бесплатно), что позволяет работать модификаторам, таким как .onDelete().

Confused Vorlon 29.07.2020 17:46

@ConfusedVorlon о, действительно! Я обновил ответ, чтобы сделать data и content общедоступными, как в случае с исходным ForEach (я оставил id как есть), и добавил соответствие DynamicViewContent. Спасибо за предложение!

Stéphane Copin 29.07.2020 18:44

Спасибо за обновления. Не уверен, что это важно, но ForEach немного ограничивает соответствие расширению ForEach: DynamicViewContent, где Content: View {}

Confused Vorlon 29.07.2020 18:51

@ConfusedVorlon Я тоже не уверен, трудно сказать, не зная внутренней работы здесь. Добавлю на всякий случай; Я не думаю, что кто-то будет использовать это без View как Content ха-ха

Stéphane Copin 29.07.2020 18:53

Небольшой нюанс: data, content и id можно объявлять как константы.

Murlakatam 05.10.2021 09:56

Преимущество следующего подхода в том, что представления в 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 убедиться, что анимация работает правильно.

Senseful 29.06.2020 18:28

ForEach(Array(array.enumerated()), id: \.1) {индекс, элемент в...}

MichaelMao 30.08.2020 09:10

что означает \.1?

Luca 05.10.2020 17:06

@Luca Array(array.enumerated()) на самом деле имеет тип EnumeratedSequence<[ItemType]>, а его элемент является кортежем типа (offset: Int, element: ItemType). Написание \.1 означает, что вы обращаетесь к 1-му (в человеческом языке - 2-му) элементу кортежа, а именно - element: ItemType. Это то же самое, что написать \.element, но короче и, по-моему, гораздо менее понятно.

ramzesenok 08.10.2020 14:50

Использование \.element имеет дополнительное преимущество, заключающееся в том, что swiftUI может определить, когда содержимое массива изменилось, даже если размер не изменился. Используйте \.элемент!

smakus 02.03.2021 01:02

Да, используйте \.element, как упоминал @ramzesenok, и реализуйте протокол Hashable, если вы используете настраиваемый массив объектов, и добавьте свойства, которые могут измениться/обновиться внутри вашего объекта внутри func hash(into hasher: inout Hasher) {, и добавьте, например, в моем случае обновление статуса, например status.hash(into: &hasher)

Fernando Romiti 26.01.2022 02:42

Для этой цели я создал специальный 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
}

Из любопытства, почему бы вам не использовать массив с отсчетом от нуля? Каков контекст?

blwinters 02.01.2022 20:56

Например, индексы ArraySlice будут индексами его элементов в его родительском массиве, поэтому они не могут начинаться с 0.

Zev Eisenberg 09.02.2022 17:00

Решение 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/

Полезно, красиво сделано!

gaohomway 09.01.2022 11:07

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")
        }

это приведет к вялости, когда у вас много строк.

mayqiyue 19.01.2022 03:21

Хотя это работает, я обнаружил, что это увеличивает ваши шансы столкнуться с ужасной ошибкой «компилятор не может проверить тип этого выражения за разумное время». У onmyway133 есть отличный ответ ниже о том, как это сделать хорошо

Todd Rylaarsdam 08.03.2022 21:15

если вы не используете идентификаторы элементов, вы не можете изменить массив

malhal 08.03.2022 21:22

Как они уже упоминали, вы можете использовать array.indices для этой цели. НО помните, что индексы, которые у вас есть, начинаются с последнего элемента массива. Чтобы решить эту проблему, вы должны использовать это: array.indices.reversed() также вы должны указать идентификатор для ForEach. Вот пример:

ForEach(array.indices.reversed(), id:\.self) { index in }

Другие вопросы по теме