Swift - 按单元对 UICollectionView 进行分页,同时保持单元水平居中
Posted
技术标签:
【中文标题】Swift - 按单元对 UICollectionView 进行分页,同时保持单元水平居中【英文标题】:Swift - Paging UICollectionView by cell while keeping the cell horizontally centered 【发布时间】:2019-07-11 08:24:26 【问题描述】:首先,不确定标题是否正确或提供了最佳描述,但我不确定还能使用什么。
所以,我正在开发一个应用程序,但我遇到了在实现 UI 时卡住的部分。 基本上,我有一个 VC(下图),它可以根据我从 JSON 文件中获得的信息进行自我定位。
问题是我需要在上方有一个类似轮播的菜单,其中包含未定义数量的单元格(同样,取决于我从 JSON 文件中获得的内容)。 我决定为此使用 UICollectionView,并且成功地实现了基础知识。
但这是我卡住的部分:
-
由于选定的单元格必须始终居中,因此当第一个和最后一个单元格被选中时,我需要在单元格和安全区域之间留出一个空白区域(参见上图)。
需要分页滚动。通常,如果 UICollectionView 单元格的宽度几乎等于屏幕之一,但要求能够一次滚动一个元素(参见上面的第二个屏幕),这不会成为问题。
我试图找到类似的东西,但也许我没有找到正确的东西,因为我只能找到Paging UICollectionView by cells, not screen
另外,老实说,我从未见过有这种行为的应用程序/UICollectionView。
我在下面发布了部分代码,但它并没有太大帮助,因为它只是标准的 UICollectionView 方法。
有什么建议吗?
class PreSignupDataVC : UIViewController, UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UIPickerViewDelegate, UIPickerViewDataSource
@IBOutlet weak var cvQuestions: UICollectionView!
var questionCell : PreSignupDataQuestionCellVC!
var screenData : Array<PreSignupScreenData> = Array<PreSignupScreenData>()
var pvDataSource : [String] = []
var numberOfComponents : Int = 0
var numberOfRowsInComponent : Int = 0
var currentScreen : Int = 1
var selectedType : Int?
var selectedCell : Int = 0
var initialLastCellInsetPoint : CGFloat = 0.0
override func viewDidLoad()
super.viewDidLoad()
print("PreSignupDataVC > viewDidLoad")
initialLastCellInsetPoint = (self.view.frame.width - 170)/2
screenData = DataSingleton.sharedInstance.returnPreSignUpUIArray()[selectedType!].screenData
numberOfComponents = screenData[currentScreen - 1].controls[0].numberOfComponents!
numberOfRowsInComponent = screenData[currentScreen - 1].controls[0].controlDataSource.count
pvDataSource = screenData[currentScreen - 1].controls[0].controlDataSource
cvQuestions.register(UINib(nibName: "PreSignupDataQuestionCell",
bundle: nil),
forCellWithReuseIdentifier: "PreSignupDataQuestionCellVC")
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
print("PreSignupDataVC > collectionView > numberOfItemsInSection")
return screenData[currentScreen - 1].controls.count
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
print("PreSignupDataVC > collectionView > cellForItemAt")
questionCell = (cvQuestions.dequeueReusableCell(withReuseIdentifier: "PreSignupDataQuestionCellVC",
for: indexPath) as? PreSignupDataQuestionCellVC)!
questionCell.vQuestionCellCellContainer.layer.cornerRadius = 8.0
questionCell.lblQuestion.text = screenData[currentScreen - 1].controls[indexPath.row].cellTitle
questionCell.ivQuestionCellImage.image = UIImage(named: screenData[currentScreen - 1].controls[indexPath.row].cellUnselectedIcon!)
return questionCell
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
print("PreSignupDataVC > collectionView > didSelectItemAt")
numberOfComponents = screenData[currentScreen - 1].controls[indexPath.row].numberOfComponents!
numberOfRowsInComponent = screenData[currentScreen - 1].controls[indexPath.row].controlDataSource.count
pvDataSource = screenData[currentScreen - 1].controls[indexPath.row].controlDataSource
selectedCell = indexPath.row
pvData.reloadAllComponents()
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
print("PreSignupDataVC > collectionView > insetForSectionAt")
return UIEdgeInsets(top: 0.0, left: initialLastCellInsetPoint, bottom: 00.0, right: initialLastCellInsetPoint)
【问题讨论】:
对于即时解决方案,您可以使用 iCarousel 动画。您可以从 Google 获取。 【参考方案1】:#1。使用UICollectionViewCompositionalLayout
(需要 ios 13)
从 iOS 13 开始,您可以使用UICollectionViewCompositionalLayout
并将NSCollectionLayoutSection
的orthogonalScrollingBehavior
属性设置为.groupPagingCentered
,以获得居中的水平轮播式布局。
以下 Swift 5.1 示例代码显示了UICollectionViewCompositionalLayout
的可能实现,以便获得您想要的布局:
CollectionView.swift
import UIKit
class CollectionView: UICollectionView
override var safeAreaInsets: UIEdgeInsets
return UIEdgeInsets(top: super.safeAreaInsets.top, left: 0, bottom: super.safeAreaInsets.bottom, right: 0)
ViewController.swift
import UIKit
class ViewController: UIViewController
var collectionView: CollectionView!
var dataSource: UICollectionViewDiffableDataSource<Int, Int>!
override func viewDidLoad()
super.viewDidLoad()
navigationItem.title = "Collection view"
// Compositional layout
let layout = UICollectionViewCompositionalLayout(sectionProvider:
(sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1), heightDimension: .fractionalHeight(1))
let item = NSCollectionLayoutItem(layoutSize: itemSize)
item.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5)
let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalHeight(1), heightDimension: .fractionalHeight(1))
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
let section = NSCollectionLayoutSection(group: group)
section.orthogonalScrollingBehavior = UICollectionLayoutSectionOrthogonalScrollingBehavior.groupPagingCentered
return section
)
// Set collection view
collectionView = CollectionView(frame: .zero, collectionViewLayout: layout)
collectionView.backgroundColor = .systemGroupedBackground
collectionView.showsHorizontalScrollIndicator = false
collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
// View layout
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.heightAnchor.constraint(equalToConstant: 160).isActive = true
collectionView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
// Collection view diffable data source
dataSource = UICollectionViewDiffableDataSource<Int, Int>(collectionView: collectionView, cellProvider:
(collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
return cell
)
var snapshot = NSDiffableDataSourceSnapshot<Int, Int>()
snapshot.appendSections([0])
snapshot.appendItems(Array(0 ..< 5))
dataSource.apply(snapshot, animatingDifferences: false)
Cell.swift
import UIKit
class Cell: UICollectionViewCell
override init(frame: CGRect)
super.init(frame: frame)
contentView.backgroundColor = .orange
required init?(coder: NSCoder)
fatalError("not implemnted")
#2。使用UICollectionViewFlowLayout
如果您的目标是低于 iOS 13 的 iOS 版本,您可以继承 UICollectionViewFlowLayout
,计算 prepare()
中的水平插入并实现 targetContentOffset(forProposedContentOffset:withScrollingVelocity:)
,以便在用户滚动后强制单元格居中。
以下 Swift 5.1 示例代码展示了如何实现UICollectionViewFlowLayout
的子类:
ViewController.swift
import UIKit
class ViewController: UIViewController
let flowLayout = PaggedFlowLayout()
override func viewDidLoad()
super.viewDidLoad()
navigationItem.title = "Collection view"
let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout)
collectionView.backgroundColor = .systemGroupedBackground
collectionView.showsHorizontalScrollIndicator = false
collectionView.decelerationRate = .fast
collectionView.dataSource = self
collectionView.contentInsetAdjustmentBehavior = .never
collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.heightAnchor.constraint(equalToConstant: 160).isActive = true
collectionView.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
extension ViewController: UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int
return 9
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
return cell
PaggedFlowLayout.swift
import UIKit
class PaggedFlowLayout: UICollectionViewFlowLayout
override init()
super.init()
scrollDirection = .horizontal
minimumLineSpacing = 5
minimumInteritemSpacing = 0
sectionInset = UIEdgeInsets(top: 5, left: 5, bottom: 5, right: 5)
required init?(coder aDecoder: NSCoder)
fatalError("init(coder:) has not been implemented")
override func prepare()
super.prepare()
guard let collectionView = collectionView else fatalError()
// itemSize
let itemHeight = collectionView.bounds.height - sectionInset.top - sectionInset.bottom
itemSize = CGSize(width: itemHeight, height: itemHeight)
// horizontal insets
let horizontalInsets = (collectionView.bounds.width - itemSize.width) / 2
sectionInset.left = horizontalInsets
sectionInset.right = horizontalInsets
/*
Add some snapping behaviour to center the cell after scrolling.
Source: https://***.com/a/14291208/1966109
*/
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint
guard let collectionView = collectionView else return .zero
var proposedContentOffset = proposedContentOffset
var offsetAdjustment = CGFloat.greatestFiniteMagnitude
let horizontalCenter = proposedContentOffset.x + collectionView.bounds.size.width / 2
let targetRect = CGRect(x: proposedContentOffset.x, y: 0, width: collectionView.bounds.size.width, height: collectionView.bounds.size.height)
guard let layoutAttributesArray = super.layoutAttributesForElements(in: targetRect) else return .zero
for layoutAttributes in layoutAttributesArray
let itemHorizontalCenter = layoutAttributes.center.x
if abs(itemHorizontalCenter - horizontalCenter) < abs(offsetAdjustment)
offsetAdjustment = itemHorizontalCenter - horizontalCenter
var nextOffset = proposedContentOffset.x + offsetAdjustment
let snapStep = itemSize.width + minimumLineSpacing
func isValidOffset(_ offset: CGFloat) -> Bool
let minContentOffset = -collectionView.contentInset.left
let maxContentOffset = collectionView.contentInset.left + collectionView.contentSize.width - itemSize.width
return offset >= minContentOffset && offset <= maxContentOffset
repeat
proposedContentOffset.x = nextOffset
let deltaX = proposedContentOffset.x - collectionView.contentOffset.x
let velX = velocity.x
if deltaX.sign.rawValue * velX.sign.rawValue != -1
break
nextOffset += CGFloat(velocity.x.sign.rawValue) * snapStep
while isValidOffset(nextOffset)
return proposedContentOffset
Cell.swift
import UIKit
class Cell: UICollectionViewCell
override init(frame: CGRect)
super.init(frame: frame)
contentView.backgroundColor = .orange
required init?(coder: NSCoder)
fatalError("not implemnted")
在 iPhone 11 Pro Max 上显示:
【讨论】:
【参考方案2】:您可以使用
更新
您可以使用滚动视图。
第一:在滚动视图中添加前导和尾随:insetForSectionAtIndex
在第一个和最后一个单元格处添加间距
第二:scrollView.clipsToBounds = false
第三:给滚动视图添加视图
func setupScrollView()
DispatchQueue.main.async
self.scrollView.layoutIfNeeded() // in order to get correct frame
let numberItem = 6
let spaceBetweenView: CGFloat = 20
let firstAndLastSpace: CGFloat = spaceBetweenView / 2
let width = self.scrollView.frame.size.width
let height = self.scrollView.frame.size.height
let numberOfView: CGFloat = CGFloat(numberItem)
let scrollViewContentSizeWidth = numberOfView * width
self.scrollView.contentSize = CGSize(width: scrollViewContentSizeWidth, height: height)
for index in 0..<numberItem
let xCoordinate = (CGFloat(index) * (width)) + firstAndLastSpace
let viewFrame = CGRect(x: xCoordinate, y: 0, width: width - spaceBetweenView, height: height)
let view = UIView()
view.backgroundColor = .red
view.frame = viewFrame
self.scrollView.addSubview(view)
【讨论】:
谢谢。这解决了插入部分。我用最新的变化更新了代码。 @daydr3am3r 抱歉,我没有阅读您的所有问题。我已经更新了答案。 谢谢。有用。但是,即使我像您一样添加了确切的约束,我也会收到警告。 “ScrollView 的可滚动内容大小不明确”。事实上,无论我尝试什么,我仍然得到同样的警告。有任何想法吗?另外,根据视图的标签,我在将 UITapGestureRecognizer 附加到 scrollView 内的每个视图时遇到了一些问题。 对于点击手势,view可以添加自己,并给vc发送回调。 vc 将按模型或您传递给查看的东西来处理它。 嗨。我通过将 ScrollView 放置在 UIView 中来解决约束问题。我还进行了一些更改以使其看起来不错并点击每个 UIView - pastebin.com/4EcjeNqH 谢谢。以上是关于Swift - 按单元对 UICollectionView 进行分页,同时保持单元水平居中的主要内容,如果未能解决你的问题,请参考以下文章
Table Cell 中的 UICollection 视图在 Swift 3 中不显示图像