Я пытаюсь создать представление, похожее на то, что мы видим на главном экране приложения Netflix для iOS, с несколькими рядами ячеек.
Вот структура моих взглядов:
Проблема, с которой я столкнулся, связана с презентациями листов. Если в главном представлении я быстро покажу подробное представление элемента 1, коснувшись прямоугольника «Коллекция 1», закрою его, перетащив вниз, а затем сразу попытаюсь отобразить подробное представление элемента 2, нажав на «Коллекцию 2», Подробный вид Коллекции 2 не отображается. Если я продолжу нажимать на разные элементы в разных коллекциях, в какой-то момент появится одно из подробных представлений. После его закрытия все ранее предпринятые попытки просмотра подробностей начинают появляться один за другим, сразу после каждого закрытия.
Такое поведение иногда происходит, даже если я не нажимаю быстро.
Для отладки я пересобрал более простую версию и попытался воспроизвести проблему, вот код:
Структура элемента
struct CollectionModel: Identifiable {
let id: UUID = UUID()
let randomNumb: Int = Int.random(in: 1...10)
static var mockArray: [CollectionModel] {
[
CollectionModel(),
CollectionModel(),
CollectionModel(),
CollectionModel(),
CollectionModel(),
CollectionModel(),
CollectionModel(),
CollectionModel(),
CollectionModel(),
CollectionModel()
]
}
}
МейнВью()
struct ContentView: View {
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
LazyVStack {
ListView()
ListView()
ListView()
ListView()
}
}.padding(.horizontal, 2)
}
}
Посмотреть список()
struct ListView: View {
var listOfCollectionModel: [CollectionModel] = CollectionModel.mockArray
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack{
ForEach(listOfCollectionModel) { model in
CollectionView(collectionModel: model)
}
}.padding(.horizontal)
}
}
}
КоллекцияView()
struct CollectionView: View {
@State var collectionModel: CollectionModel
@State var showDetails: Bool = false
var body: some View {
Text("\(collectionModel.randomNumb)")
.padding()
.frame(width: 200, height: 250)
.background(.red.gradient)
.clipShape(RoundedRectangle(cornerRadius: 15))
.font(.title)
.bold()
.foregroundStyle(.white)
.onTapGesture {
showDetails = true
}
.sheet(isPresented: $showDetails){
ZStack{
Color.red
.ignoresSafeArea()
Text("\(collectionModel.randomNumb)")
}
}
}
}
После просмотра некоторого руководства по .sheet() выяснилось, что, возможно, лучше использовать .sheet(item). Итак, я попробовал это следующим образом:
Посмотреть список()
struct ListView: View {
var listOfCollectionModel: [CollectionModel] = CollectionModel.mockArray
@State var selectedModel: CollectionModel? = nil
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack{
ForEach(listOfCollectionModel) { model in
CollectionView(collectionModel: model)
.onTapGesture {
selectedModel = model
}
}
}.padding(.horizontal)
.sheet(item: $selectedModel) { model in
ZStack {
Color.black
.ignoresSafeArea()
Text("\(model.randomNumb)")
.font(.title)
.bold()
.foregroundStyle(.white)
}
}
}
}
}
КоллекцияView()
struct CollectionView: View {
@State var collectionModel: CollectionModel
var body: some View {
Text("\(collectionModel.randomNumb)")
.padding()
.frame(width: 200, height: 250)
.background(.red.gradient)
.clipShape(RoundedRectangle(cornerRadius: 15))
.font(.title)
.bold()
.foregroundStyle(.white)
}
}
Теперь лист представлен хорошо, но иногда содержимое может совпадать с ранее выбранным содержимым. Чтобы избежать этой проблемы, я использовал .id(sheetID) следующим образом:
Посмотреть список()
struct ListView: View {
var listOfCollectionModel: [CollectionModel] = CollectionModel.mockArray
@State var selectedModel: CollectionModel? = nil
@State var sheetID: UUID = UUID()
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack{
ForEach(listOfCollectionModel) { model in
CollectionView(collectionModel: model)
.onTapGesture {
selectedModel = model
sheetID = UUID()
}
}
}.padding(.horizontal)
.sheet(item: $selectedModel) { model in
ZStack {
Color.black
.ignoresSafeArea()
Text("\(model.randomNumb)")
.font(.title)
.bold()
.foregroundStyle(.white)
}.id(sheetID)
}
}
}
}
Теперь мне кажется, что это работает, но я не знаю... Мне действительно кажется, что я сделал что-то неправильно вначале, и теперь я понемногу исправляю свою первоначальную ошибку (которую я не знаю, в чем она заключается). немного, добавляя слои ненужных исправлений.
Ребята, у вас есть какие-нибудь мысли по этому поводу? Спасибо за вашу помощь.
Ответ от @Benzy Neez работает хорошо.





Лист отображается поверх всего остального содержимого, поэтому часто бывает полезно прикрепить лист к постоянно присутствующему представлению, например к родительскому контейнеру.
В исходной версии кода вы прикрепляли лист к каждому CollectionView. Причина, по которой лист иногда не отображался, может заключаться в конфликтах из-за дублирования и фрагментации.
В обновленной версии вы прикрепляете лист к LazyHStack в каждом ListView. Это означает, что все еще действуют четыре модификатора .sheet.
Также может быть важно, что модификаторы .sheet прикреплены к представлениям, которые находятся внутри контейнера с отложенной загрузкой (ListView вложены в LazyVStack). Это тоже может быть причиной проблемы.
Я бы предложил такие изменения:
.sheet к внешнему ScrollView..id из ZStack внутри листа.selectedModel как привязку к представлениям ребенка.let вместо var, когда это возможно.@State. Переменные состояния всегда должны быть локальными и частными.Вот обновленная версия с примененными этими изменениями:
struct CollectionView: View {
let collectionModel: CollectionModel
var body: some View {
Text("\(collectionModel.randomNumb)")
// ...
}
}
struct ListView: View {
let listOfCollectionModel: [CollectionModel] = CollectionModel.mockArray
@Binding var selectedModel: CollectionModel?
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack{
// ...
}
.padding(.horizontal)
// .sheet removed
}
}
}
struct ContentView: View {
@State private var selectedModel: CollectionModel? = nil
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
LazyVStack {
ListView(selectedModel: $selectedModel)
ListView(selectedModel: $selectedModel)
ListView(selectedModel: $selectedModel)
ListView(selectedModel: $selectedModel)
}
}
.padding(.horizontal, 2)
.sheet(item: $selectedModel) { model in
ZStack {
// ...
}
}
}
}
Я ожидаю, что это будет работать надежно.
РЕДАКТИРОВАТЬ В своем комментарии вы сказали, что попробовали изменения, а также переместили жест касания на основной CollectionView. Ничего страшного, если вы передадите selectedModel в качестве обязательного.
У меня это работает именно так. Вот полный код, который вы можете просто скопировать/вставить для тестирования/сравнения:
struct CollectionView: View {
let collectionModel: CollectionModel
@Binding var selectedModel: CollectionModel?
var body: some View {
Text("\(collectionModel.randomNumb)")
.padding()
.frame(width: 200, height: 250)
.background(.red.gradient)
.clipShape(RoundedRectangle(cornerRadius: 15))
.font(.title)
.bold()
.foregroundStyle(.white)
.onTapGesture {
selectedModel = collectionModel
}
}
}
struct ListView: View {
let listOfCollectionModel: [CollectionModel] = CollectionModel.mockArray
@Binding var selectedModel: CollectionModel?
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack{
ForEach(listOfCollectionModel) { model in
CollectionView(collectionModel: model, selectedModel: $selectedModel)
}
}
.padding(.horizontal)
}
}
}
struct ContentView: View {
@State private var selectedModel: CollectionModel? = nil
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
LazyVStack {
ListView(selectedModel: $selectedModel)
ListView(selectedModel: $selectedModel)
ListView(selectedModel: $selectedModel)
ListView(selectedModel: $selectedModel)
}
}
.padding(.horizontal, 2)
.sheet(item: $selectedModel) { model in
ZStack {
Color.black
.ignoresSafeArea()
Text("\(model.randomNumb)")
.font(.title)
.bold()
.foregroundStyle(.white)
}
}
}
}
@Kaiye У меня это работает, ответ обновлен с полным кодом.
Я реализовал ваше решение непосредственно в своем основном проекте, поэтому, если оно вам подходит, оно должно быть напрямую связано с моим основным проектом. Я отвечу кодом моего основного проекта как можно скорее.
Я отредактировал основной ответ с основным кодом проекта с учетом вашего совета.
@Kaiye Трудно понять, почему этот код не работает, потому что он неполный. Однако в ForEach в SeasonView мне интересно, почему вы используете поиск по массиву, чтобы найти массив Planime для перехода к AnimeListView, вместо использования параметра замыкания animeSeason? Кроме того, в AnimeListView вы используете индексы массива вместо перебора идентифицируемых элементов, так что Planime не реализуется Identifiable? Если это так, вы уверены, что нет дубликатов? Кстати, вы пробовали упрощенную версию из моего ответа, и она у вас тоже сработала?
Planime идентифицируется, я исправил. Спасибо, что указали на это. Что касается ForEach в SeasonView, аниме выпускаются по сезонам, в зависимости от того, когда вы будете использовать приложение, я хочу, чтобы в первой строке был текущий сезон -1. поэтому в моей модели представления есть логика, которая управляет этим и правильно упорядочивает сезон в зависимости от того, когда вы открываете приложение. Что привело к этому поиску для раскопок массива. Закрытие AnimeSeason — это название сезона, и список сортируется не по ключевому слову (сезон), а по индексу (0,1,2,3). Я протестировал ваш код, и он хорошо работает в упрощенной версии.
Я добавил .onchange(of: selectedAnime) { oldValue, newValue in. Поведение очень ясное: если я не подожду хотя бы 1 секунду, чтобы выбранное аниме вернулось к нулю, то следующий CollectionView( ), который я нажму, покажет текущее выбранное аниме (непосредственно перед его изменением). Он заполняется, как жест перетаскивания, чтобы закрыть представление и вернуть выбранное аниме в ноль, это очень долго.
@Kaiye Спасибо за подтверждение того, что решение исходного вопроса подойдет и вам. Так что, возможно, вы могли бы рассмотреть возможность принятия ответа? 😉 Не уверен, что смогу помочь вам гораздо больше, не имея для изучения всей вашей базы кода. Однако есть одна мысль: убедитесь, что AnimePreview определяет anime как свойство let или, точнее, не как переменную @State.
Привет, твой ответ был правильным. На самом деле все работает хорошо, у меня только одна функция в коде AnimePreview() работает неправильно (изображение не меняется, но все остальное содержимое меняется хорошо, и эта картинка меня загипнотизировала...). Думаю, теперь я смогу справиться с этим в одиночку. Большое спасибо за Вашу помощь!
Перемещение .sheet() в родительское представление, чтобы иметь только один лист, имеет смысл. Я последовал вашему совету и передал привязку selectedModel из представления в дочернее представление: ListView(), CollectionView(). Я также поместил жест касания, который устанавливает выбранную модель в коллекциюView(). Это вообще не работает, мне нужно дважды нажать на ячейки, чтобы получить хороший DetailView(). (первая итерация всегда работает, но следующая всегда показывает сначала выбранное ранее представление). Могу ли я дать вам больше моего кода?