title: SwiftUI How do I record the index of the grid item that is currently being displayed on CollectionView?
body: <p>I am trying to have a scrolling horizontal list of items, where each item takes up almost the full screen width (UIScreen.main.bounds.width - 50).  There should be just enough of the next item visible for the user to know there is something to scroll to.  I'd like to be able to determine the index of the item that's currently taking up most of the view.</p>
  <p>The main view has three subviews: a search bar, a map, and a results view (which is where I want the scrolling horizontal list).  The pins on the map need to update based on the currently displayed result.</p>
  <p>I have included all code from the project for clarity and reproducibility.</p>
  <p>The main view:</p>
  <pre><code>import SwiftUI
  
  struct ContentView: View
  {
    @State var results = [[Place]]()
    @State var selectedResult = [Place]()
    
      var body: some View {
        VStack(alignment: .center) {
          SearchBar(results: $results)
            .padding()
          
          SearchMapView(result: $selectedResult)
            .frame(height: UIScreen.main.bounds.height/3)
          
          SearchResultsView(results: $results, selectedResult: $selectedResult)
          
          
          Spacer()
        }
      }
  }
  </code></pre>
  <p>Search Bar:</p>
  <pre><code>import SwiftUI
  
  struct SearchBar: View
  {
    @State private var text: String = &quot;&quot;
    @Binding var results: [[Place]]
    
    var body: some View {
      HStack {
        TextField(&quot;Search&quot;, text: $text)
        
        Button(action: { findGroup() }, label: {
          Image(systemName: &quot;magnifyingglass&quot;)
        })
      }
    }
    
    func findGroup()
    {
      var foundResults = [[Place]]()
      for vacation in vacations
      {
        var resultFound = false
              for place in vacation
        {
          if !resultFound
          {
            let name = place.name.uppercased()
            if name.contains(text.uppercased())
            {
              foundResults.append(vacation)
              resultFound = true
            }
          }
        }
        results = foundResults
      }
    }
  }
  </code></pre>
  <p>Map:</p>
  <pre><code>import SwiftUI
  import MapKit
  
  struct SearchMapView: View
  {
    // MARK: - Properties
    @State private var region = MKCoordinateRegion(
      center: CLLocationCoordinate2D(
        latitude: 37.0902,
        longitude: -95.7129
      ),
      span: MKCoordinateSpan(
        latitudeDelta: 1,
        longitudeDelta: 1
      )
    )
    
    @Binding var result: [Place]
    
    // MARK: - View
      var body: some View {
        Map(coordinateRegion: $region, annotationItems: result) { place in
          MapMarker(coordinate: CLLocationCoordinate2D(latitude: place.latitude , longitude: place.longitude ))
        }
        .onAppear {
          findCenter()
        }
        .onChange(of: result, perform: { _ in
          findCenter()
        })
        
        .ignoresSafeArea(edges: .horizontal)
      }
    
    // MARK: - Methods
    func findCenter()
    {
      if let place = result.first
      {
        region.center = CLLocationCoordinate2D(latitude: place.latitude , longitude: place.longitude )
      }
    }
  }
  </code></pre>
  <p>The results view:</p>
  <pre><code>import SwiftUI
  
  struct SearchResultsView: View
  {
    // MARK: - Properties
    typealias Row = CollectionRow&lt;Int, [Place]&gt;
    @State var rows: [Row] = []
    @State var resultDetailIsPresented: Bool = false
    @State var selectedResultNeedsUpdate: Bool = false
    
    @Binding var results: [[Place]]
    @Binding var selectedResult: [Place]
    
    // MARK: - View
    var body: some View {
      VStack(alignment: .leading) {
        HStack {
          Text(&quot;Results&quot;)
            .font(.headline)
          
          ZStack {
            Circle()
              .foregroundColor(.gray)
              .frame(width: 25, height: 25)
            
            Text(&quot;\(results.count)&quot;)
              .bold()
              .accessibility(identifier: &quot;results count&quot;)
            
            Spacer()
          } //: Count ZStack
          .hidden(results.isEmpty)
          
        } //: Heading HStack
        .padding(.leading)
        
        Divider()
        
        if !results.isEmpty
        {
          CollectionViewUI(rows: rows) { sectionIndex, layoutEnvironment in
            createSection()
          } cell: { indexPath, result in
            if let place = result.first
            {
              button(place: place)
                .border(Color.black, width: 1)
            }
          } //: Collection View Cell
          
        } else
        {
          Text(&quot;No current results.&quot;)
            .padding(.leading)
        } // Else
        
        Spacer()
      } // Main VStack
      .onChange(of: results, perform: { _ in
        print(&quot;Results have changed.&quot;)
        fillRows()
        selectedResultNeedsUpdate = true
      })
      .onChange(of: selectedResultNeedsUpdate, perform: { value in
        if value == true // This still causes &quot;Modifying state during view update&quot; error, but the state saves.
        {
          updateSelection()
          selectedResultNeedsUpdate = false
        }
      })
  
      .sheet(isPresented: $resultDetailIsPresented, content: {
        Text(&quot;Result: \(selectedResult.first?.name ?? &quot;Missing.&quot;)&quot;)
      })
    }
    
    // MARK: - Methods
    func fillRows()
    {
      rows = []
      
      rows.append(Row(section: 0, items: results))
    }
    
    func createSection() -&gt; NSCollectionLayoutSection
    {
      let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
      let item = NSCollectionLayoutItem(layoutSize: itemSize)
      
      let groupSize = NSCollectionLayoutSize(widthDimension: .estimated(UIScreen.main.bounds.width - 50), heightDimension: .estimated(UIScreen.main.bounds.height/3))
      let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
      
      let section = NSCollectionLayoutSection(group: group)
      section.contentInsets = NSDirectionalEdgeInsets(top: 20, leading: 0, bottom: 20, trailing: 0)
      section.interGroupSpacing = 20
      section.orthogonalScrollingBehavior = .groupPagingCentered
      return section
    }
    
    func updateSelection()
    {
      if !results.isEmpty
      {
        selectedResult = results[0] // Temporary solution so -something- is selected
        print(&quot;Selected result \(selectedResult.first?.name ?? &quot;missing.&quot;)&quot;)
      } else
      {
        print(&quot;Results are empty.&quot;)
      }
    }
    
    func button(place: Place) -&gt; some View
    {
      GeometryReader { geometry in
        Button(action: {
          resultDetailIsPresented = true
          
        }) { //: Button Action
          ResultCardView(place: place)
        } //: Button Content
      } //: Geo
      .frame(maxHeight: .infinity)
      .ignoresSafeArea(.keyboard, edges: .bottom)
    }
  }
  
  extension View
  {
    /// Use a Bool to determine whether or not a view should be hidden.
    /// - Parameter shouldHide: Bool
    /// - Returns: some View
    @ViewBuilder func hidden(_ shouldHide: Bool) -&gt; some View {
      switch shouldHide
      {
        case true:
          self.hidden()
        case false:
          withAnimation {
            self.animation(.easeOut(duration: 0.5))
          }
      }
    }
  }
  </code></pre>
  <p>Result Card View</p>
  <pre><code>import SwiftUI
  
  struct ResultCardView: View
  {
    let screenWidth = UIScreen.main.bounds.width
    var place: Place
    
      var body: some View {
        HStack(alignment: .top) {
          
            Image(systemName: &quot;car&quot;)
              .resizable()
              .aspectRatio(contentMode: .fit)
              .frame(width: 150)
              .padding()
              .foregroundColor(.black)
  
          VStack(alignment: .leading) {
            Text(&quot;Place&quot;)
            
            Text(&quot;\(place.name))&quot;)
            
            Spacer()
          } //: Result Main VStack
          .padding()
        } //: Result Main HStack
        
        .frame(width: screenWidth - 50)
        .ignoresSafeArea(edges: .horizontal)
      }
  }
  </code></pre>
  <p>Model</p>
  <pre><code>import MapKit
  
  struct Place: Identifiable, Equatable, Hashable
  {
    let id = UUID()
    var name: String
    var latitude: Double
    var longitude: Double
    
    var coordinate: CLLocationCoordinate2D {
      CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
    }
  }
  </code></pre>
  <p>Mock Data</p>
  <pre><code>// Florida
  var magicKingdom = Place(
    name: &quot;Magic Kingdom&quot;,
    latitude: 28.4177,
    longitude: -81.5812)
  var epcot = Place(
    name: &quot;Epcot&quot;,
    latitude: 28.3747,
    longitude: -81.5494)
  var buschGardens = Place(
    name: &quot;Busch Gardens&quot;,
    latitude: 28.0372,
    longitude: -82.4194)
  var universal = Place(
    name: &quot;Universal Studios&quot;,
    latitude: 28.4754,
    longitude: -81.4677)
  var animalKingdom = Place(
    name: &quot;Animal Kingdom&quot;,
    latitude: 28.3529,
    longitude: -81.5907)
  
  var vacation1: [Place] = [
    magicKingdom,
    epcot,
    animalKingdom]
  var vacation2: [Place] = [
    magicKingdom,
    epcot,
    animalKingdom,
    buschGardens,
    universal]
  var vacation3: [Place] = [epcot, buschGardens]
  var vacation4: [Place] = [universal, buschGardens]
  var vacation5: [Place] = [buschGardens]
  
  // California
  var appleCampus = Place(
    name: &quot;Apple Campus&quot;,
    latitude: 37.33182,
    longitude: -122.03118)
  var disneyLand = Place(
    name: &quot;Disney Land&quot;,
    latitude: 33.8121,
    longitude: -117.9190)
  var goldenGate = Place(
    name: &quot;Golden Gate Bridge&quot;,
    latitude: 37.8199,
    longitude: -122.4783)
  var alcatraz = Place(
    name: &quot;Alcatraz&quot;,
    latitude: 37.8270,
    longitude: -122.4230)
  var coit = Place(
    name: &quot;Coit Tower&quot;,
    latitude: 37.8024,
    longitude: -122.4058)
  
  var vacation6: [Place] = [
    appleCampus,
    disneyLand,
    goldenGate,
    alcatraz,
    coit]
  var vacation7: [Place] = [disneyLand]
  var vacation8: [Place] = [
    appleCampus,
    goldenGate,
    coit]
  var vacation9: [Place] = [disneyLand, alcatraz]
  var vacation10: [Place] = [coit, appleCampus]
  
  var vacations: [[Place]] = [
    vacation1,
    vacation2,
    vacation3,
    vacation4,
    vacation5,
    vacation6,
    vacation7,
    vacation8,
    vacation9,
    vacation10]
  </code></pre>
  <p>Here is the CollectionView converted with UIViewRepresentable.  This was based on a <a href="https://defagos.github.io/swiftui_collection_intro/" rel="nofollow noreferrer">blog post</a> by Samuel Defago.</p>
  <pre><code>import SwiftUI
  
  public struct CollectionViewUI&lt;Section: Hashable, Item: Hashable, Cell: View&gt;: UIViewRepresentable
  {
    // MARK: - Properties
    let rows: [CollectionRow&lt;Section, Item&gt;]
    let sectionLayoutProvider: (Int, NSCollectionLayoutEnvironment) -&gt; NSCollectionLayoutSection
    let cell: (IndexPath, Item) -&gt; Cell
    
    // MARK: - Initializer
    public init(rows: [CollectionRow&lt;Section, Item&gt;],
         sectionLayoutProvider: @escaping (Int, NSCollectionLayoutEnvironment) -&gt; NSCollectionLayoutSection,
         @ViewBuilder cell: @escaping (IndexPath, Item) -&gt; Cell) {
      self.rows = rows
      self.sectionLayoutProvider = sectionLayoutProvider
      self.cell = cell
    }
    
    // MARK: - Helpers
    enum Section: Hashable
    {
      case main
    }
    
    private class HostCell: UICollectionViewCell
    {
      private var hostController: UIHostingController&lt;Cell&gt;?
      
      override func prepareForReuse()
      {
        if let hostView = hostController?.view
        {
          hostView.removeFromSuperview()
        }
        hostController = nil
      }
      
      var hostedCell: Cell? {
        willSet {
          guard let view = newValue else { return }
          hostController = UIHostingController(rootView: view, ignoreSafeArea: true)
          if let hostView = hostController?.view
          {
            hostView.frame = contentView.bounds
            hostView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
            contentView.addSubview(hostView)
          }
        }
      }
    }
    
    public class CVCoordinator: NSObject, UICollectionViewDelegate
    {
      fileprivate typealias DataSource = UICollectionViewDiffableDataSource&lt;Section, Item&gt;
      
      fileprivate var isFocusable: Bool = false
      fileprivate var dataSource: DataSource? = nil
      fileprivate var rowsHash: Int? = nil
      fileprivate var sectionLayoutProvider: ((Int, NSCollectionLayoutEnvironment) -&gt; NSCollectionLayoutSection)?
      
      public func collectionView(_ collectionView: UICollectionView, canFocusItemAt indexPath: IndexPath) -&gt; Bool
      {
        return isFocusable
      }
    }
    
    // MARK: - Methods
    // View instantiation
    public func makeUIView(context: Context) -&gt; UICollectionView
    {
      let cellIdentifier = &quot;hostCell&quot;
      
      let collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout(context: context))
      collectionView.backgroundColor = .systemBackground
      collectionView.register(HostCell.self, forCellWithReuseIdentifier: cellIdentifier)
      collectionView.showsVerticalScrollIndicator = false
      
      context.coordinator.dataSource = Coordinator.DataSource(collectionView: collectionView) { collectionView, indexPath, item in
        let hostCell = collectionView.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: indexPath) as? HostCell
        hostCell?.hostedCell = cell(indexPath, item)
        return hostCell
      }
      
      reloadData(in: collectionView, context: context)
      return collectionView
    }
    
    // Updating View
    public func updateUIView(_ uiView: UICollectionView, context: Context)
    {
      reloadData(in: uiView, context: context, animated: true)
    }
    
    // Coordinator
    public func makeCoordinator() -&gt; CVCoordinator
    {
      CVCoordinator()
    }
    
    // Create Layout
    private func layout(context: Context) -&gt; UICollectionViewLayout
    {
      let layout = UICollectionViewCompositionalLayout { sectionIndex, layoutEnvironment in
        context.coordinator.sectionLayoutProvider!(sectionIndex, layoutEnvironment)
      }
      return layout
    }
    
    // Reload data
    private func reloadData(in collectionView: UICollectionView, context: Context, animated: Bool = false)
    {
      let coordinator = context.coordinator
      coordinator.sectionLayoutProvider = self.sectionLayoutProvider
      
      guard let dataSource = context.coordinator.dataSource else { return }
      let rowsHash = rows.hashValue // TODO: Determine if we want to keep this as hash comparison
      if coordinator.rowsHash != rowsHash
      {
        dataSource.apply(snapshot(), animatingDifferences: animated)
        coordinator.isFocusable = true
        collectionView.setNeedsFocusUpdate()
        collectionView.updateFocusIfNeeded()
        coordinator.isFocusable = false
      }
      coordinator.rowsHash = rowsHash
    }
    
    // Create snapshot
    private func snapshot() -&gt; NSDiffableDataSourceSnapshot&lt;Section, Item&gt;
    {
      var snapshot = NSDiffableDataSourceSnapshot&lt;Section, Item&gt;()
      for row in rows
      {
        snapshot.appendSections([row.section])
        snapshot.appendItems(row.items, toSection: row.section)
      }
      return snapshot
    }
  }
  
  public struct CollectionRow&lt;Section: Hashable, Item: Hashable&gt;: Hashable
  {
    let section: Section
    let items: [Item]
  }
  
  // Fixes frames so they are a consistent size.
  extension UIHostingController
  {
    convenience public init(rootView: Content, ignoreSafeArea: Bool)
    {
      self.init(rootView: rootView)
      
      if ignoreSafeArea
      {
        disableSafeArea()
      }
    }
    
    func disableSafeArea()
    {
      guard let viewClass = object_getClass(view) else { return }
      
      let viewSubclassName = String(cString: class_getName(viewClass)).appending(&quot;_IgnoreSafeArea&quot;)
      if let viewSubclass = NSClassFromString(viewSubclassName) {
        object_setClass(view, viewSubclass)
      } else
      {
        guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
        guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
        
        if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets))
        {
          let safeAreaInsets: @convention(block) (AnyObject) -&gt; UIEdgeInsets = { _ in
            return .zero
          }
          class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
        }
        
        objc_registerClassPair(viewSubclass)
        object_setClass(view, viewSubclass)
      }
    }
  }
  </code></pre>