如何解决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 举报,一经查实,本站将立刻删除。