SwiftUI Как мне записать индекс элемента сетки, который в данный момент отображается в Stack/LazyHGrid с горизонтальной прокруткой?
Я пытаюсь создать прокручиваемый горизонтальный список элементов, где каждый элемент занимает почти всю ширину экрана (UIScreen.main.bounds.width - 50). Следующего элемента должно быть достаточно, чтобы пользователь знал, что есть к чему прокрутить. В идеале я бы хотел, чтобы список привязывался к центральному элементу. Но я-ИМЕЮ- иметь возможность определить индекс элемента, который в настоящее время занимает большую часть просмотра.
В представлении у меня три подпредставления: панель поиска, карта и представление результатов (именно там я хочу прокручиваемый горизонтальный список). Пины на карте необходимо обновить в зависимости от текущего отображаемого результата.
Основной вид:
struct SearchView: View
{
// MARK: - Properties
@State var results = [[Read]]()
@State var selectedIndex: Int = 0
@State var selectedResult = [Read]()
// MARK: - View
var body: some View {
VStack {
SearchBar(results: $results)
.padding()
SearchMapView(result: selectedResult)
.frame(height: (UIScreen.main.bounds.height/3))
SearchResultsView(results: $results, selectedIndex: $selectedIndex)
}
.onAppear
{
if results.count > 1
{
selectedResult = results[selectedIndex]
}
}
.ignoresSafeArea(.keyboard, edges: .bottom)
}
}
Просмотр результатов:
struct SearchResultsView: View
{
@Binding var results: [[Read]]
@Binding var selectedIndex: Int
var rows: [GridItem] = [GridItem(.flexible())]
var body: some View {
VStack(alignment: .leading) {
HStack {
Text("Results")
ZStack {
Circle()
.foregroundColor(.gray)
.frame(width: 25, height: 25)
Text("\(results.count)")
.accessibility(identifier: "results count")
Spacer()
} //: Count ZStack
.hidden(results.isEmpty)
} //: Heading HStack
.padding(.leading)
Divider()
if !results.isEmpty {
ScrollView(.horizontal) {
// LazyHGrid(rows: rows, content: {
/* I've tried using a LazyHGrid and an HStack with a custom modifier. I wasn't able to get either method to work */
HStack {
ForEach(Array(results.enumerated()), id: \.1) { index, result in
if let read = result.first
{
ResultCardView(read: read, index: index)
} // If
} //: ForEach
} // HStack
// .modifier(HScroll(itemCount: results.count, itemSpacing: 20, selectedIndex: $selectedIndex))
/*I attempted to make a custom modifier, but it squished the cards, and still didn't calculate the index for the current card.*\
// }) // LazyHGrid
} // Scrollview
.frame(maxHeight: .infinity)
.ignoresSafeArea(.keyboard, edges: .bottom)
} else
{
Text("No current results.")
.padding(.leading)
} // Else
Spacer()
} // Main VStack
}
}
Вот модификатор HStack, который я пытался реализовать. Начальное смещение слишком далеко влево, поэтому весь контент начинается за пределами экрана вправо. Кроме того, он редко привязывается к текущему элементу. Исходная версия этого модификатора была найдена здесь https://levelup.gitconnected.com/snap-to-item-scrolling-debccdcbb22f
struct HScroll: ViewModifier
{
let screenWidth: CGFloat
let itemWidth: CGFloat
let contentWidth: CGFloat
let itemCount: Int
let itemSpacing: CGFloat
@Binding var selectedIndex: Int
@State private var scrollOffset: CGFloat = 0
@State private var dragOffset: CGFloat = 0
init(itemCount: Int, itemSpacing: CGFloat, selectedIndex: Binding<Int>) {
self.screenWidth = UIScreen.main.bounds.width
self.itemCount = itemCount
self.itemSpacing = itemSpacing
self._selectedIndex = selectedIndex
// Calculate Total Content Width
self.itemWidth = screenWidth - 50
self.contentWidth = CGFloat(itemCount) * itemWidth + CGFloat(itemCount - 1) * itemSpacing
// Set Initial Offset to first Item
let initialOffset = (contentWidth/2.0) - (screenWidth/2.0) + ((screenWidth - itemWidth) / 2.0)
self._scrollOffset = State(initialValue: initialOffset)
self._dragOffset = State(initialValue: 0)
}
func body(content: Content) -> some View {
content
.offset(x: scrollOffset + dragOffset, y: 0)
.gesture(DragGesture()
.onChanged({ event in
dragOffset = event.translation.width
})
.onEnded({ event in
// Scroll to where user dragged
scrollOffset += event.translation.width
dragOffset = 0
// Center position of current offset
let center = scrollOffset + (screenWidth / 2.0) + (contentWidth / 2.0)
// Calculate which item we are closest to using the defined size
var index = (center - (screenWidth / 2.0)) / (itemWidth + itemSpacing)
// Should we stay at current index or are we closer to the next item...
if index.remainder(dividingBy: 1) > 0.5 {
index += 1
}
// Protect from scrolling out of bounds
index = min(index, CGFloat(itemCount) - 1)
index = max(index, 0)
print("The index is \(index).")
// Set final offset (snapping to item)
let newOffset = index * itemWidth + (index - 1) * itemSpacing - (contentWidth / 2.0) + (screenWidth / 2.0) - ((screenWidth - itemWidth) / 2.0) + itemSpacing
// Animate snapping
withAnimation {
scrollOffset = newOffset
selectedIndex = Int(index - 1)
printValues()
}
})
)
}
func printValues()
{
print("Snapping to item.")
print("Screen Width: \(screenWidth)")
print("Item Width: \(itemWidth)")
print("Content Width: \(contentWidth)")
print("Item Count: \(itemCount)")
print("Item Spacing: \(itemSpacing)")
print("Selected Index: \(selectedIndex)")
print("Scroll Offset \(scrollOffset)")
print("Drag Offset \(dragOffset)")
}
}
1 ответ
Нашел супер простое решение этой проблемы. Это делает привязку к элементу и передает индекс, как и в старом представлении коллекции.
Я добавил @State var selection: Int = 0 в ContentView и привязки "selection" к карте и представлению результатов.
Затем я заменил раздел Контроллер представления коллекции следующим образом:
TabView(selection: $selection) {
ForEach(Array(zip(results.indices, results)), id: \.0) { index, result in
ResultCardView(place: result[0]).tag(index)
}
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .always))
Он делает именно то, что я хочу, и на реализацию у меня ушло пять минут. Я нашел это решение здесь: https://swiftwithmajid.com/2020/09/16/tabs-and-pages-in-swiftui/