N 个 Collection View 单元格 - 自动调整大小以适合屏幕中的所有单元格
N number of Collection View cells - autoresize to fir all cells in screen
我需要始终将我的集合视图单元格放在 iphone 屏幕边界内。
它应该像这样工作:
- 如果一个单元格,它应该填满整个屏幕
- 如果有两个单元格,第一个单元格应填充前半部分,第二个单元格应填充后半部分
- 如果是三个单元格,屏幕应分成 4 个相等的单元格,第 4 个单元格占位符应为空。如果是第 4 个单元格,它应该填充空 space.
- 如果 five/six 个单元格,则应该有三行,每行两个单元格。
我有一个逻辑,但通过一些 hack,我设法只支持 6 个单元格,之后它就不起作用了。
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var horizontalTiles = 1
var verticalTiles = 1
var targetWidth = collectionView.frame.width
var targetHeight = collectionView.frame.height - UIApplication.shared.statusBarFrame.height
horizontalTiles = horizontalTiles+(self.contactsArray.count/3)
verticalTiles = verticalTiles+(self.contactsArray.count/2)
if self.contactsArray.count%4 == 0 || self.contactsArray.count%6 == 0 {
return lastCellSize
}
targetWidth = targetWidth / CGFloat(horizontalTiles)
targetHeight = targetHeight / CGFloat(verticalTiles)
let size = CGSize(width: targetWidth, height: targetHeight)
lastCellSize = size
return size
}
如果有更好的方法或者使用流式布局会很有帮助。
将动画放在一边...您可以通过一些巧妙的方法获得尺寸 math/logic(您的问题专门询问尺寸)。
一些observations/assumptions:
- 所有单元格大小相同,所以我们可以return一个大小
- rows/columns交替递增的数量
- 行在列之前递增
- 大小保持不变,直到所有列都填满最后一行
鉴于此...
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let numItems = self.contactsArray.count
let targetWidth = collectionView.frame.width
let targetHeight = collectionView.frame.height - UIApplication.shared.statusBarFrame.height
if numItems == 1 {
return CGSize(width: targetWidth, height: targetHeight)
}
// Below logic only works for numItems > 1, so we check for that ^
var numRows = 1
var numColumns = 1
for index in 1...(numItems-1) {
if index % numColumns == 0 {
if numColumns == numRows {
numRows += 1
} else {
numColumns += 1
}
}
}
return CGSize(width: targetWidth / numColumns, height: targetHeight / numRows)
}
如果您不习惯使用集合视图,这里有另一种方法。
考虑行和列。行数将是总项目的平方根的上限,列数将是总项目除以行数的上限。
然后您可以轻松计算出“平铺”尺寸。
示例代码:
class ArrangeViewController: UIViewController {
// Add a view button
let addButton: UIButton = {
let v = UIButton()
v.setTitle("Add", for: .normal)
return v
}()
// Remove a view button
let remButton: UIButton = {
let v = UIButton()
v.setTitle("Remove", for: [])
return v
}()
// horizontal stackview to hold the buttons
let btnsStack: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fillEqually
v.spacing = 20
return v
}()
// view to hold the added views
let tilesContainerView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .systemRed
v.clipsToBounds = true
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
// button properties
[addButton, remButton].forEach { b in
b.backgroundColor = .yellow
b.setTitleColor(.blue, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
}
// add the buttons to the stack view
btnsStack.addArrangedSubview(addButton)
btnsStack.addArrangedSubview(remButton)
// add buttons stack to the view
view.addSubview(btnsStack)
// add border container to the view
view.addSubview(tilesContainerView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain buttons stack Top / Leading / Trailing with a little "padding"
btnsStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 12.0),
btnsStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
btnsStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// buttons height to 40-pts (just for asthetics)
btnsStack.heightAnchor.constraint(equalToConstant: 40.0),
// constrain border container
// 20-pts below buttons
tilesContainerView.topAnchor.constraint(equalTo: btnsStack.bottomAnchor, constant: 20.0),
// 20-pts from view bottom
tilesContainerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
// 20-pts Leading and Trailing
tilesContainerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
tilesContainerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
// add actions for the Add and Delete buttons
addButton.addTarget(self, action: #selector(addTapped(_:)), for: .touchUpInside)
remButton.addTarget(self, action: #selector(remTapped(_:)), for: .touchUpInside)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
}) { [unowned self] _ in
self.arrangeViews()
}
}
@objc func addTapped(_ sender: Any?) -> Void {
// instantiate a new custom view and add it to
// the inner container view
let v = MyArrangeView()
tilesContainerView.addSubview(v)
v.theLabel.text = "\(tilesContainerView.subviews.count)"
// update the arrangement
arrangeViews()
}
@objc func remTapped(_ sender: Any?) -> Void {
// if inner container has at least one custom view
if let v = tilesContainerView.subviews.last {
// remove it
v.removeFromSuperview()
// update the arrangement
arrangeViews()
}
}
func arrangeViews() -> Void {
// make sure there is at least 1 subview to arrange
guard tilesContainerView.subviews.count > 0 else { return }
// init local vars to use
// Note: making them all CGFLoats makes it easier to use in expressions - avoids a lot of casting CGFloat(var)
var numCols: CGFloat = 0
var numRows: CGFloat = 0
var w: CGFloat = 0
var h: CGFloat = 0
// this is the frame we need to fit inside
let containerWidth: CGFloat = tilesContainerView.frame.size.width
let containerHeight: CGFloat = tilesContainerView.frame.size.height
// number of views to arrange
let numTiles: CGFloat = CGFloat(tilesContainerView.subviews.count)
// get the ceil of the square root of number of tiles
// that's the number of rows
numRows = CGFloat(ceil(sqrt(numTiles)))
// get the ceil of the number of tiles divided by number of rows
// that's the number of columns
numCols = CGFloat(ceil(numTiles / numRows))
// size of each tile
w = containerWidth / numCols
h = containerHeight / numRows
var x: CGFloat = 0.0
var y: CGFloat = 0.0
UIView.animate(withDuration: 0.3, animations: {
// loop through, doing the actual layout (setting each item's frame)
self.tilesContainerView.subviews.forEach { v in
v.frame = CGRect(x: x, y: y, width: w, height: h)
x += w
if x + w > containerWidth + 1 {
x = 0.0
y += h
}
}
self.view.layoutIfNeeded()
})
}
}
// simple bordered custom view with a label in a "content container"
class MyArrangeView: UIView {
// this will hold the "content" of the custom view
// for this example, it just holds a label
let theContentView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .systemTeal
return v
}()
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .clear
v.textAlignment = .center
v.font = UIFont.systemFont(ofSize: 14.0)
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.backgroundColor = .systemYellow
// add the label to the content view
theContentView.addSubview(theLabel)
// add the content view to self
addSubview(theContentView)
NSLayoutConstraint.activate([
// constrain the label to all 4 sides of the content view
theLabel.topAnchor.constraint(equalTo: theContentView.topAnchor, constant: 0.0),
theLabel.bottomAnchor.constraint(equalTo: theContentView.bottomAnchor, constant: 0.0),
theLabel.leadingAnchor.constraint(equalTo: theContentView.leadingAnchor, constant: 0.0),
theLabel.trailingAnchor.constraint(equalTo: theContentView.trailingAnchor, constant: 0.0),
// constrain the content view to all 4 sides of self with 5-pts "padding"
theContentView.topAnchor.constraint(equalTo: topAnchor, constant: 5.0),
theContentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5.0),
theContentView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5.0),
theContentView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5.0),
])
layer.borderWidth = 1
layer.borderColor = UIColor.systemGray.cgColor
}
}
并且,旋转...
我需要始终将我的集合视图单元格放在 iphone 屏幕边界内。
它应该像这样工作:
- 如果一个单元格,它应该填满整个屏幕
- 如果有两个单元格,第一个单元格应填充前半部分,第二个单元格应填充后半部分
- 如果是三个单元格,屏幕应分成 4 个相等的单元格,第 4 个单元格占位符应为空。如果是第 4 个单元格,它应该填充空 space.
- 如果 five/six 个单元格,则应该有三行,每行两个单元格。
我有一个逻辑,但通过一些 hack,我设法只支持 6 个单元格,之后它就不起作用了。
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
var horizontalTiles = 1
var verticalTiles = 1
var targetWidth = collectionView.frame.width
var targetHeight = collectionView.frame.height - UIApplication.shared.statusBarFrame.height
horizontalTiles = horizontalTiles+(self.contactsArray.count/3)
verticalTiles = verticalTiles+(self.contactsArray.count/2)
if self.contactsArray.count%4 == 0 || self.contactsArray.count%6 == 0 {
return lastCellSize
}
targetWidth = targetWidth / CGFloat(horizontalTiles)
targetHeight = targetHeight / CGFloat(verticalTiles)
let size = CGSize(width: targetWidth, height: targetHeight)
lastCellSize = size
return size
}
如果有更好的方法或者使用流式布局会很有帮助。
将动画放在一边...您可以通过一些巧妙的方法获得尺寸 math/logic(您的问题专门询问尺寸)。
一些observations/assumptions:
- 所有单元格大小相同,所以我们可以return一个大小
- rows/columns交替递增的数量
- 行在列之前递增
- 大小保持不变,直到所有列都填满最后一行
鉴于此...
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let numItems = self.contactsArray.count
let targetWidth = collectionView.frame.width
let targetHeight = collectionView.frame.height - UIApplication.shared.statusBarFrame.height
if numItems == 1 {
return CGSize(width: targetWidth, height: targetHeight)
}
// Below logic only works for numItems > 1, so we check for that ^
var numRows = 1
var numColumns = 1
for index in 1...(numItems-1) {
if index % numColumns == 0 {
if numColumns == numRows {
numRows += 1
} else {
numColumns += 1
}
}
}
return CGSize(width: targetWidth / numColumns, height: targetHeight / numRows)
}
如果您不习惯使用集合视图,这里有另一种方法。
考虑行和列。行数将是总项目的平方根的上限,列数将是总项目除以行数的上限。
然后您可以轻松计算出“平铺”尺寸。
示例代码:
class ArrangeViewController: UIViewController {
// Add a view button
let addButton: UIButton = {
let v = UIButton()
v.setTitle("Add", for: .normal)
return v
}()
// Remove a view button
let remButton: UIButton = {
let v = UIButton()
v.setTitle("Remove", for: [])
return v
}()
// horizontal stackview to hold the buttons
let btnsStack: UIStackView = {
let v = UIStackView()
v.translatesAutoresizingMaskIntoConstraints = false
v.axis = .horizontal
v.alignment = .fill
v.distribution = .fillEqually
v.spacing = 20
return v
}()
// view to hold the added views
let tilesContainerView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .systemRed
v.clipsToBounds = true
return v
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBlue
// button properties
[addButton, remButton].forEach { b in
b.backgroundColor = .yellow
b.setTitleColor(.blue, for: .normal)
b.setTitleColor(.lightGray, for: .highlighted)
}
// add the buttons to the stack view
btnsStack.addArrangedSubview(addButton)
btnsStack.addArrangedSubview(remButton)
// add buttons stack to the view
view.addSubview(btnsStack)
// add border container to the view
view.addSubview(tilesContainerView)
let g = view.safeAreaLayoutGuide
NSLayoutConstraint.activate([
// constrain buttons stack Top / Leading / Trailing with a little "padding"
btnsStack.topAnchor.constraint(equalTo: g.topAnchor, constant: 12.0),
btnsStack.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
btnsStack.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
// buttons height to 40-pts (just for asthetics)
btnsStack.heightAnchor.constraint(equalToConstant: 40.0),
// constrain border container
// 20-pts below buttons
tilesContainerView.topAnchor.constraint(equalTo: btnsStack.bottomAnchor, constant: 20.0),
// 20-pts from view bottom
tilesContainerView.bottomAnchor.constraint(equalTo: g.bottomAnchor, constant: -20.0),
// 20-pts Leading and Trailing
tilesContainerView.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
tilesContainerView.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
])
// add actions for the Add and Delete buttons
addButton.addTarget(self, action: #selector(addTapped(_:)), for: .touchUpInside)
remButton.addTarget(self, action: #selector(remTapped(_:)), for: .touchUpInside)
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { _ in
}) { [unowned self] _ in
self.arrangeViews()
}
}
@objc func addTapped(_ sender: Any?) -> Void {
// instantiate a new custom view and add it to
// the inner container view
let v = MyArrangeView()
tilesContainerView.addSubview(v)
v.theLabel.text = "\(tilesContainerView.subviews.count)"
// update the arrangement
arrangeViews()
}
@objc func remTapped(_ sender: Any?) -> Void {
// if inner container has at least one custom view
if let v = tilesContainerView.subviews.last {
// remove it
v.removeFromSuperview()
// update the arrangement
arrangeViews()
}
}
func arrangeViews() -> Void {
// make sure there is at least 1 subview to arrange
guard tilesContainerView.subviews.count > 0 else { return }
// init local vars to use
// Note: making them all CGFLoats makes it easier to use in expressions - avoids a lot of casting CGFloat(var)
var numCols: CGFloat = 0
var numRows: CGFloat = 0
var w: CGFloat = 0
var h: CGFloat = 0
// this is the frame we need to fit inside
let containerWidth: CGFloat = tilesContainerView.frame.size.width
let containerHeight: CGFloat = tilesContainerView.frame.size.height
// number of views to arrange
let numTiles: CGFloat = CGFloat(tilesContainerView.subviews.count)
// get the ceil of the square root of number of tiles
// that's the number of rows
numRows = CGFloat(ceil(sqrt(numTiles)))
// get the ceil of the number of tiles divided by number of rows
// that's the number of columns
numCols = CGFloat(ceil(numTiles / numRows))
// size of each tile
w = containerWidth / numCols
h = containerHeight / numRows
var x: CGFloat = 0.0
var y: CGFloat = 0.0
UIView.animate(withDuration: 0.3, animations: {
// loop through, doing the actual layout (setting each item's frame)
self.tilesContainerView.subviews.forEach { v in
v.frame = CGRect(x: x, y: y, width: w, height: h)
x += w
if x + w > containerWidth + 1 {
x = 0.0
y += h
}
}
self.view.layoutIfNeeded()
})
}
}
// simple bordered custom view with a label in a "content container"
class MyArrangeView: UIView {
// this will hold the "content" of the custom view
// for this example, it just holds a label
let theContentView: UIView = {
let v = UIView()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .systemTeal
return v
}()
let theLabel: UILabel = {
let v = UILabel()
v.translatesAutoresizingMaskIntoConstraints = false
v.backgroundColor = .clear
v.textAlignment = .center
v.font = UIFont.systemFont(ofSize: 14.0)
return v
}()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
func commonInit() -> Void {
self.backgroundColor = .systemYellow
// add the label to the content view
theContentView.addSubview(theLabel)
// add the content view to self
addSubview(theContentView)
NSLayoutConstraint.activate([
// constrain the label to all 4 sides of the content view
theLabel.topAnchor.constraint(equalTo: theContentView.topAnchor, constant: 0.0),
theLabel.bottomAnchor.constraint(equalTo: theContentView.bottomAnchor, constant: 0.0),
theLabel.leadingAnchor.constraint(equalTo: theContentView.leadingAnchor, constant: 0.0),
theLabel.trailingAnchor.constraint(equalTo: theContentView.trailingAnchor, constant: 0.0),
// constrain the content view to all 4 sides of self with 5-pts "padding"
theContentView.topAnchor.constraint(equalTo: topAnchor, constant: 5.0),
theContentView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -5.0),
theContentView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 5.0),
theContentView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -5.0),
])
layer.borderWidth = 1
layer.borderColor = UIColor.systemGray.cgColor
}
}
并且,旋转...