Несколько презентаций .sheet(): интеграция быстрого закрытия/представления

Я пытаюсь создать представление, похожее на то, что мы видим на главном экране приложения Netflix для iOS, с несколькими рядами ячеек.

Вот структура моих взглядов:

  • CollectionView(): отображает отдельные элементы (в виде прямоугольника).
  • ListOfCollections(): отображает горизонтальную прокрутку элементов CollectionView.
  • MainView(): использует несколько экземпляров ListOfCollections() для создания нескольких строк.
  • CollectionDetailsView(): подробное представление соответствующей коллекции.
  • Каждая ячейка может представлять подробное представление в виде листа с помощью .sheet(isPresented:Content:).

Проблема, с которой я столкнулся, связана с презентациями листов. Если в главном представлении я быстро покажу подробное представление элемента 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 работает хорошо.

Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
57
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

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

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

  • В исходной версии кода вы прикрепляли лист к каждому 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)
            }
        }
    }
}

Перемещение .sheet() в родительское представление, чтобы иметь только один лист, имеет смысл. Я последовал вашему совету и передал привязку selectedModel из представления в дочернее представление: ListView(), CollectionView(). Я также поместил жест касания, который устанавливает выбранную модель в коллекциюView(). Это вообще не работает, мне нужно дважды нажать на ячейки, чтобы получить хороший DetailView(). (первая итерация всегда работает, но следующая всегда показывает сначала выбранное ранее представление). Могу ли я дать вам больше моего кода?

Kaiye 29.05.2024 20:51

@Kaiye У меня это работает, ответ обновлен с полным кодом.

Benzy Neez 29.05.2024 21:04

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

Kaiye 29.05.2024 21:20

Я отредактировал основной ответ с основным кодом проекта с учетом вашего совета.

Kaiye 29.05.2024 21:35

@Kaiye Трудно понять, почему этот код не работает, потому что он неполный. Однако в ForEach в SeasonView мне интересно, почему вы используете поиск по массиву, чтобы найти массив Planime для перехода к AnimeListView, вместо использования параметра замыкания animeSeason? Кроме того, в AnimeListView вы используете индексы массива вместо перебора идентифицируемых элементов, так что Planime не реализуется Identifiable? Если это так, вы уверены, что нет дубликатов? Кстати, вы пробовали упрощенную версию из моего ответа, и она у вас тоже сработала?

Benzy Neez 29.05.2024 21:59

Planime идентифицируется, я исправил. Спасибо, что указали на это. Что касается ForEach в SeasonView, аниме выпускаются по сезонам, в зависимости от того, когда вы будете использовать приложение, я хочу, чтобы в первой строке был текущий сезон -1. поэтому в моей модели представления есть логика, которая управляет этим и правильно упорядочивает сезон в зависимости от того, когда вы открываете приложение. Что привело к этому поиску для раскопок массива. Закрытие AnimeSeason — это название сезона, и список сортируется не по ключевому слову (сезон), а по индексу (0,1,2,3). Я протестировал ваш код, и он хорошо работает в упрощенной версии.

Kaiye 29.05.2024 23:48

Я добавил .onchange(of: selectedAnime) { oldValue, newValue in. Поведение очень ясное: если я не подожду хотя бы 1 секунду, чтобы выбранное аниме вернулось к нулю, то следующий CollectionView( ), который я нажму, покажет текущее выбранное аниме (непосредственно перед его изменением). Он заполняется, как жест перетаскивания, чтобы закрыть представление и вернуть выбранное аниме в ноль, это очень долго.

Kaiye 30.05.2024 00:06

@Kaiye Спасибо за подтверждение того, что решение исходного вопроса подойдет и вам. Так что, возможно, вы могли бы рассмотреть возможность принятия ответа? 😉 Не уверен, что смогу помочь вам гораздо больше, не имея для изучения всей вашей базы кода. Однако есть одна мысль: убедитесь, что AnimePreview определяет anime как свойство let или, точнее, не как переменную @State.

Benzy Neez 30.05.2024 09:08

Привет, твой ответ был правильным. На самом деле все работает хорошо, у меня только одна функция в коде AnimePreview() работает неправильно (изображение не меняется, но все остальное содержимое меняется хорошо, и эта картинка меня загипнотизировала...). Думаю, теперь я смогу справиться с этим в одиночку. Большое спасибо за Вашу помощь!

Kaiye 30.05.2024 15:46

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