微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

SwiftUI 如何记录当前在 CollectionView 上显示的网格项的索引?

如何解决SwiftUI 如何记录当前在 CollectionView 上显示的网格项的索引?

我正在尝试创建一个滚动的水平项目列表,其中每个项目几乎占据整个屏幕宽度 (UIScreen.main.bounds.width - 50)。应该有足够的下一个项目让用户知道有什么可以滚动到的。我希望能够确定当前占据大部分视图的项目的索引。

主视图有三个子视图:搜索栏、地图和结果视图(这是我想要滚动水平列表的地方)。地图上的图钉需要根据当前显示的结果进行更新。

为了清晰和可重现,我已经包含了项目中的所有代码

主视图:

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()
      }
    }
}

搜索栏:

import SwiftUI

struct SearchBar: View
{
  @State private var text: String = ""
  @Binding var results: [[Place]]
  
  var body: some View {
    HStack {
      TextField("Search",text: $text)
      
      Button(action: { findGroup() },label: {
        Image(systemName: "magnifyingglass")
      })
    }
  }
  
  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
    }
  }
}

地图:

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 )
    }
  }
}

结果视图:

import SwiftUI

struct SearchResultsView: View
{
  // MARK: - Properties
  typealias Row = CollectionRow<Int,[Place]>
  @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("Results")
          .font(.headline)
        
        ZStack {
          Circle()
            .foregroundColor(.gray)
            .frame(width: 25,height: 25)
          
          Text("\(results.count)")
            .bold()
            .accessibility(identifier: "results count")
          
          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("No current results.")
          .padding(.leading)
      } // Else
      
      Spacer()
    } // Main vstack
    .onChange(of: results,perform: { _ in
      print("Results have changed.")
      fillRows()
      selectedResultNeedsUpdate = true
    })
    .onChange(of: selectedResultNeedsUpdate,perform: { value in
      if value == true // This still causes "Modifying state during view update" error,but the state saves.
      {
        updateSelection()
        selectedResultNeedsUpdate = false
      }
    })

    .sheet(isPresented: $resultDetailIsPresented,content: {
      Text("Result: \(selectedResult.first?.name ?? "Missing.")")
    })
  }
  
  // MARK: - Methods
  func fillRows()
  {
    rows = []
    
    rows.append(Row(section: 0,items: results))
  }
  
  func createSection() -> 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("Selected result \(selectedResult.first?.name ?? "missing.")")
    } else
    {
      print("Results are empty.")
    }
  }
  
  func button(place: Place) -> 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) -> some View {
    switch shouldHide
    {
      case true:
        self.hidden()
      case false:
        withAnimation {
          self.animation(.eaSEOut(duration: 0.5))
        }
    }
  }
}

结果卡视图

import SwiftUI

struct ResultCardView: View
{
  let screenWidth = UIScreen.main.bounds.width
  var place: Place
  
    var body: some View {
      HStack(alignment: .top) {
        
          Image(systemName: "car")
            .resizable()
            .aspectRatio(contentMode: .fit)
            .frame(width: 150)
            .padding()
            .foregroundColor(.black)

        vstack(alignment: .leading) {
          Text("Place")
          
          Text("\(place.name))")
          
          Spacer()
        } //: Result Main vstack
        .padding()
      } //: Result Main HStack
      
      .frame(width: screenWidth - 50)
      .ignoresSafeArea(edges: .horizontal)
    }
}

型号

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)
  }
}

模拟数据

// Florida
var magicKingdom = Place(
  name: "Magic Kingdom",latitude: 28.4177,longitude: -81.5812)
var epcot = Place(
  name: "Epcot",latitude: 28.3747,longitude: -81.5494)
var buschGardens = Place(
  name: "Busch Gardens",latitude: 28.0372,longitude: -82.4194)
var universal = Place(
  name: "Universal Studios",latitude: 28.4754,longitude: -81.4677)
var animalKingdom = Place(
  name: "Animal Kingdom",latitude: 28.3529,longitude: -81.5907)

var vacation1: [Place] = [
  magicKingdom,epcot,animalKingdom]
var vacation2: [Place] = [
  magicKingdom,animalKingdom,buschGardens,universal]
var vacation3: [Place] = [epcot,buschGardens]
var vacation4: [Place] = [universal,buschGardens]
var vacation5: [Place] = [buschGardens]

// California
var appleCampus = Place(
  name: "Apple Campus",latitude: 37.33182,longitude: -122.03118)
var disneyLand = Place(
  name: "disney Land",latitude: 33.8121,longitude: -117.9190)
var goldenGate = Place(
  name: "Golden Gate Bridge",latitude: 37.8199,longitude: -122.4783)
var alcatraz = Place(
  name: "Alcatraz",latitude: 37.8270,longitude: -122.4230)
var coit = Place(
  name: "Coit Tower",latitude: 37.8024,longitude: -122.4058)

var vacation6: [Place] = [
  appleCampus,disneyLand,goldenGate,alcatraz,coit]
var vacation7: [Place] = [disneyLand]
var vacation8: [Place] = [
  appleCampus,coit]
var vacation9: [Place] = [disneyLand,alcatraz]
var vacation10: [Place] = [coit,appleCampus]

var vacations: [[Place]] = [
  vacation1,vacation2,vacation3,vacation4,vacation5,vacation6,vacation7,vacation8,vacation9,vacation10]

这里是用 UIViewRepresentable 转换的 CollectionView。这是基于 Samuel Defago 的 blog post

import SwiftUI

public struct CollectionViewUI<Section: Hashable,Item: Hashable,Cell: View>: UIViewRepresentable
{
  // MARK: - Properties
  let rows: [CollectionRow<Section,Item>]
  let sectionLayoutProvider: (Int,NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection
  let cell: (IndexPath,Item) -> Cell
  
  // MARK: - Initializer
  public init(rows: [CollectionRow<Section,Item>],sectionLayoutProvider: @escaping (Int,NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection,@viewbuilder cell: @escaping (IndexPath,Item) -> 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<Cell>?
    
    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<Section,Item>
    
    fileprivate var isFocusable: Bool = false
    fileprivate var dataSource: DataSource? = nil
    fileprivate var rowsHash: Int? = nil
    fileprivate var sectionLayoutProvider: ((Int,NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection)?
    
    public func collectionView(_ collectionView: UICollectionView,canFocusItemAt indexPath: IndexPath) -> Bool
    {
      return isFocusable
    }
  }
  
  // MARK: - Methods
  // View instantiation
  public func makeUIView(context: Context) -> UICollectionView
  {
    let cellIdentifier = "hostCell"
    
    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() -> CVCoordinator
  {
    CVCoordinator()
  }
  
  // Create Layout
  private func layout(context: Context) -> 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() -> NSDiffableDataSourceSnapshot<Section,Item>
  {
    var snapshot = NSDiffableDataSourceSnapshot<Section,Item>()
    for row in rows
    {
      snapshot.appendSections([row.section])
      snapshot.appendItems(row.items,toSection: row.section)
    }
    return snapshot
  }
}

public struct CollectionRow<Section: Hashable,Item: Hashable>: 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("_IgnoreSafeArea")
    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) -> UIEdgeInsets = { _ in
          return .zero
        }
        class_addMethod(viewSubclass,#selector(getter: UIView.safeAreaInsets),imp_implementationWithBlock(safeAreaInsets),method_getTypeEncoding(method))
      }
      
      objc_registerClasspair(viewSubclass)
      object_setClass(view,viewSubclass)
    }
  }
}

解决方法

为这个问题找到了一个超级简单的解决方案。这会对齐项目并传递索引,就像旧的集合视图一样。

我在 ContentView 中添加了一个 @State var selection: Int = 0,并在地图和结果视图中添加了“选择”绑定。

然后我用这个替换了 Collection View Controller 部分:

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/

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。