Представление не перерисовывается во вложенном цикле ForEach

У меня есть следующий компонент, который отображает сетку полупрозрачных символов:

    var body: some View {
        VStack{
            Text("\(self.settings.numRows) x \(self.settings.numColumns)")
            ForEach(0..<self.settings.numRows){ i in
                Spacer()
                    HStack{
                        ForEach(0..<self.settings.numColumns){ j in
                            Spacer()
                            // why do I get an error when I try to multiply i * j
                            self.getSymbol(index:j)
                            Spacer()
                        }
                    }
                Spacer()
            }
        }
    }

settings это EnvironmentObject

Всякий раз, когда settings обновляется текст во внешнем VStackправильно обновляется. Однако остальная часть представления не обновляется (сетка имеет те же размеры, что и раньше). Почему это?

Второй вопрос: почему нет доступа к i во внутреннем ForEach-loop и передать его в качестве аргумента функции?

Я получаю ошибку на внешнем ForEach-петля:

Не удалось вывести универсальный параметр "Данные"

2 ответа

Решение

TL;DR

Ваши потребности ForEach id: \.self добавлен после вашего диапазона.

Объяснение

ForEach имеет несколько инициализаторов. Ты используешь

init(_ data: Range<Int>, @ViewBuilder content: @escaping (Int) -> Content)

где data должно быть константой.

Если ваш диапазон может измениться (например, вы добавляете или удаляете элементы из массива, что изменит верхнюю границу), вам необходимо использовать

init(_ data: Data, id: KeyPath<Data.Element, ID>, content: @escaping (Data.Element) -> Content)

Вы указываете путь к id параметр, который однозначно определяет каждый элемент, ForEachпетли. В случаеRange<Int>, элемент, который вы перебираете, является Intуказание индекса массива, который является уникальным. Поэтому вы можете просто использовать\.self keypath, чтобы ForEach идентифицировал каждый элемент индекса по его собственному значению.

Вот как это выглядит на практике:

struct ContentView: View {
    @State var array = [1, 2, 3]

    var body: some View {
        VStack {
            Button("Add") {
                self.array.append(self.array.last! + 1)
            }

            // this is the key part  v--------v
            ForEach(0..<array.count, id: \.self) { index in
                Text("\(index): \(self.array[index])")
                //Note: If you want more than one views here, you need a VStack or some container, or will throw errors
            }
        }
    }
}

Если вы запустите это, вы увидите, что при нажатии кнопки добавления элементов в массив они появятся в VStackавтоматически. Если вы удалите "id: \.self", вы увидите исходную ошибку:

`ForEach(_:content:)` should only be used for *constant* data. 
Instead conform data to `Identifiable` or use `ForEach(_:id:content:)`
and provide an explicit `id`!"

ForEach следует использовать только для постоянных данных. Таким образом, по определению он оценивается только один раз. Попробуйте обернуть его в список, и вы увидите, как регистрируются ошибки, например:

ForEach, Int, TupleView<(Spacer, HStack, Int, TupleView <(Spacer, Text, Spacer)>>>, Spacer)>> count (7)!= Его начальное количество (0). ForEach(_:content:)следует использовать только для постоянных данных. Вместо этого согласовывайте данные сIdentifiable или используйте ForEach(_:id:content:) и предоставить явное id!

Меня это тоже удивило, и я не смог найти никакой официальной документации об этом ограничении.

Что касается того, почему вы не можете получить доступ к i во внутреннем цикле ForEach, я думаю, что у вас, вероятно, есть вводящая в заблуждение ошибка компилятора, связанная с чем-то еще в коде, который отсутствует в ваших фрагментах. Он действительно скомпилировался для меня после того, как я дополнил недостающие части (Xcode 11.1, Mac OS 10.14.4).

Вот что я придумал, чтобы ваш ForEach прошел через что-то идентифицируемое:



struct SettingsElement: Identifiable {
    var id: Int { value }

    let value: Int

    init(_ i: Int) { value = i }
}

class Settings: ObservableObject {
    @Published var rows = [SettingsElement(0),SettingsElement(1),SettingsElement(2)]
    @Published var columns = [SettingsElement(0),SettingsElement(1),SettingsElement(2)]
}

struct ContentView: View {
    @EnvironmentObject var settings: Settings

    func getSymbol(index: Int) -> Text { Text("\(index)") }

    var body: some View {
        VStack{
            Text("\(self.settings.rows.count) x \(self.settings.columns.count)")
            ForEach(self.settings.rows) { i in
                VStack {
                    HStack {
                        ForEach(self.settings.columns) { j in
                            Text("\(i.value) \(j.value)")
                        }
                    }
                }
            }
        }
    }
}

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