Как связать массив и список, если массив является членом ObservableObject?

Я хочу создать MyViewModel, который получает данные из сети, а затем обновляет массив результатов. MyView должен подписаться на $model.results и показать List с заполненными результатами.

К сожалению, я получаю сообщение об ошибке «Тип выражения неоднозначен без дополнительного контекста».

Как правильно использовать ForEach в этом случае?

import SwiftUI
import Combine

class MyViewModel: ObservableObject {
    @Published var results: [String] = []

    init() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
            self.results = ["Hello", "World", "!!!"]
        }
    }
}

struct MyView: View {
    @ObservedObject var model: MyViewModel

    var body: some View {
        VStack {
            List {
                ForEach($model.results) { text in
                    Text(text)
                 // ^--- Type of expression is ambiguous without more context
                }
            }
        }
    }
}

struct MyView_Previews: PreviewProvider {
    static var previews: some View {
        MyView(model: MyViewModel())
    }
}

P.S. Если я заменю модель на @State var results: [String], все будет работать нормально, но мне понадобится отдельный class MyViewModel: ObservableObject для моих целей


person Denis Kreshikhin    schedule 23.09.2019    source источник


Ответы (1)


Исправление

Измените свой ForEach блок на

ForEach(model.results, id: \.self) { text in
    Text(text)
}

Объяснение

Сообщения об ошибках SwiftUI здесь не приносят никакой пользы. Настоящее сообщение об ошибке (которое вы увидите, если измените Text(text) на Text(text as String) и удалите $ перед model.results): «Не удалось вывести общий параметр 'ID'».

Другими словами, чтобы использовать ForEach, элементы, которые вы повторяете, должны быть однозначно идентифицированы одним из двух способов.

  1. Если элемент является структурой или классом, вы можете привести его в соответствие с протоколом Identifiable, добавив свойство var id: Hashable. В этом случае вам не нужен параметр id.
  2. Другой вариант - указать ForEach, что использовать в качестве уникального идентификатора, с помощью параметра id. Обновление. Вы должны гарантировать, что в вашей коллекции нет повторяющихся элементов. Если два элемента имеют одинаковый идентификатор, любые изменения, внесенные в одно представление (например, смещение), произойдут с обоими представлениями.

В этом случае мы выбрали вариант 2 и сказали ForEach использовать сам элемент String в качестве идентификатора (\.self). Мы можем это сделать, поскольку String соответствует протоколу Hashable.

А как насчет $?

Большинство представлений в SwiftUI принимают только состояние вашего приложения и строят свой внешний вид на его основе. В этом примере текстовые представления просто берут информацию, хранящуюся в модели, и отображают ее. Но некоторые представления должны иметь возможность возвращаться и изменять состояние вашего приложения в ответ на пользователя:

  • Toggle необходимо обновить значение Bool в ответ на переключение
  • Ползунок должен обновить значение Double в ответ на слайд
  • TextField необходимо обновить значение String в ответ на ввод

Мы определяем, что должна существовать двусторонняя связь между состоянием приложения и представлением, используя Binding<SomeType>. Таким образом, Toggle требует, чтобы вы передали ему Binding<Bool>, Slider требует Binding<Double>, а TextField требует Binding<String>.

Именно здесь появляется оболочка свойства @State (или @Published внутри @ObservedObject). Эта оболочка свойства «обертывает» значение, которое она содержит в Binding (вместе с некоторыми другими вещами, чтобы гарантировать, что SwiftUI знает, что нужно обновлять представления при изменении значения) . Если нам нужно получить значение, мы можем просто сослаться на myVariable, но если нам нужна привязка, мы можем использовать сокращение $myVariable.

Итак, в этом случае ваш исходный код содержал ForEach($model.results). Другими словами, вы говорили компилятору: «Итерировать Binding<[String]>», но Binding не является коллекцией, которую вы можете перебирать. Удаление $ говорит: «Итерировать по этой [String]», а Array - это коллекция, которую вы можете перебирать.

person John M.    schedule 23.09.2019
comment
Хорошо, будет ли это решение обновлять представление, когда результаты изменяются без префикса $? - person Denis Kreshikhin; 24.09.2019
comment
@DenisKreshikhin Я обновил пост, чтобы прояснить, почему вам не нужны (и вообще не могут быть) $ там. - person John M.; 24.09.2019