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/

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