I have a working list with UICollectionViewController (but the issue might be relevant for bare UICollectionView, I have not tested). It is working like Image below, but the issue is, the width varies contents.
I want the width to fill the width and let the height calculate itself based on contents. So far I have not been able to put a coherent solution. I have tried to override FlowLayout but then I have to pass the height of the cell which I don't know at that moment.
What is the solution that iOS devs use to solve such a problem. It looks to me common sense thing but apple seems to have complicated this one for no reason.
Swift 5 example of full width cells when standard UICollectionViewFlowLayout is used.
Idea behind:
We are informing UICollectionViewFlowLayout that we want to have self-sizing cells.
Then we passing current collection view width to each cell.
Cell overrides preferredLayoutAttributesFitting(...) method. Then it performs desired size calculation using passed width. Desired size calculated using systemLayoutSizeFitting(width: ...).
class MyVievControler: UICollectionViewController {
enum CellId: String {
case red, green, blue
}
let items = repeatElement([CellId.red, .green, .blue], count: 40).reduce(into: []) { $0 += $1 }
init() {
let layout = UICollectionViewFlowLayout()
// We want self-sizing cells
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
super.init(collectionViewLayout: layout)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cellId = items[indexPath.row]
switch cellId {
case .red:
let cell = collectionView.dequeueReusableCell(MyCellRed.self, indexPath: indexPath)
cell.contentWidth = collectionView.bounds.width
cell.text = indexPath.item % 2 == 0 ? StubObject().text.x128 : StubObject().text.x32
return cell
case .green:
let cell = collectionView.dequeueReusableCell(MyCellGreen.self, indexPath: indexPath)
cell.contentWidth = collectionView.bounds.width
return cell
case .blue:
let cell = collectionView.dequeueReusableCell(MyCellBlue.self, indexPath: indexPath)
cell.contentWidth = collectionView.bounds.width
return cell
}
}
}
private class MyCellRed: UICollectionViewCell {
var contentWidth: CGFloat = 100
private lazy var label = UILabel()
var text: String? {
didSet {
label.text = text
}
}
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
var newFrame = layoutAttributes.frame
// For better performance this size need to be cached as long as Cell content/model is not changed.
let desiredSize = systemLayoutSizeFitting(width: contentWidth, verticalFitting: .fittingSizeLevel)
newFrame.size = CGSize(width: contentWidth, height: desiredSize.height)
layoutAttributes.frame = newFrame
log.debug("Red: Calculated frame: \(newFrame). Desired size: \(desiredSize)")
return layoutAttributes
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundView = UIView(backgroundColor: .red)
layer.setBorder(width: 0.5)
contentView.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
anchor.pin.toBounds(insets: 8, label).activate()
label.layer.borderWidth = 1
label.numberOfLines = 0
label.textColor = .white
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private class MyCellGreen: UICollectionViewCell {
var contentWidth: CGFloat = 100
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
var newFrame = layoutAttributes.frame
newFrame.size = CGSize(width: contentWidth, height: 40)
layoutAttributes.frame = newFrame
log.debug("Green: Calculated frame: \(newFrame)")
return layoutAttributes
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundView = UIView(backgroundColor: .green)
layer.setBorder(width: 0.5)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
private class MyCellBlue: UICollectionViewCell {
var contentWidth: CGFloat = 100
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
var newFrame = layoutAttributes.frame
newFrame.size = CGSize(width: contentWidth, height: 100)
layoutAttributes.frame = newFrame
log.debug("Blue: Calculated frame: \(newFrame)")
return layoutAttributes
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundView = UIView(backgroundColor: .blue)
layer.setBorder(width: 0.5)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Result:
After reading this Article (Thanks to #DonMag), I was able to achieve what I wanted. So basically you extend the Cell class, and do some changes to CollectionView and its flowLayout. Here are the classes
FullWidthCollectionViewCell: All your cells in need of full width should extend it instead of UICollectionViewCell
class FullWidthCollectionViewCell: UICollectionViewCell
{
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
var modifiedTargetSize = targetSize
modifiedTargetSize.height = CGFloat.greatestFiniteMagnitude
let size = super.systemLayoutSizeFitting(
modifiedTargetSize,
withHorizontalFittingPriority: .required,
verticalFittingPriority: .fittingSizeLevel
)
return size
}
}
Add extension to UICollectionView class. I have single file for all my view extensions but you can put it in the same file as CollectionView code
extension UICollectionView {
var widestCellWidth: CGFloat {
let insets = contentInset.left + contentInset.right
return bounds.width - insets
}
}
Finally in your CollectionView code. In my case I was using UICollectionViewController so All is set for me. If you are using UICollectionView as view in some custom UIViewController then you must adjust the code accordingly
class MyCollectionViewController: UICollectionViewController {
override func viewDidLoad() {
super.viewDidLoad()
let layout = collectionView.collectionViewLayout
if let flowLayout = layout as? UICollectionViewFlowLayout {
flowLayout.estimatedItemSize = CGSize(
width: collectionView.widestCellWidth,
// Make the height a reasonable estimate to
// ensure the scroll bar remains smooth
height: 200
)
}
}
//Delegate and other methods here
}
That is all to enjoy. Again thanks to #DonMag for the link!
Related
I have a collection view which scrolls horizontally much like a carousel.
The height and width of all the cells should be equal to the cell which has the longest text.
It doesn't do this however and instead expands it's width depending on the length of the text which results into different widths of cells though the height remains constant.
max number of lines for the text is only 4.
I tried other answers in SO but none of them worked for me.
Here is my code, I am using SnapKit for constraints.
View Controller
import UIKit
import SnapKit
class ViewController: UIViewController {
let viewModel: [SeatViewModel] = [.init(text: "single text"), .init(text: "slightly longer text."), .init(text: "very very very very long long text")]
var collectionViewSize: CGFloat = 10
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.estimatedItemSize = CGSize(width: 200, height: 100)
let cv = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
cv.register(SeatCardCell.self, forCellWithReuseIdentifier: "SeatCardCell")
return cv
}()
override func viewDidLoad() {
super.viewDidLoad()
self.view.addSubview(collectionView)
collectionView.snp.makeConstraints { make in
make.top.equalToSuperview()
make.leading.equalToSuperview()
make.trailing.equalToSuperview()
make.height.equalTo(100)
}
collectionView.dataSource = self
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
collectionView.frame = view.bounds
}
}
extension ViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 3
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let collectioCell = collectionView.dequeueReusableCell(withReuseIdentifier: "SeatCardCell", for: indexPath) as? SeatCardCell else { fatalError() }
collectioCell.backgroundColor = [UIColor.green, .red, .black, .brown, .yellow].randomElement()
collectioCell.viewModel = viewModel[indexPath.row]
return collectioCell
}
}
Cell and View Model
struct SeatViewModel {
let text: String
}
class SeatCardCell: UICollectionViewCell {
var viewModel: SeatViewModel? {
didSet {
configure()
}
}
// MARK: - Properties
let seatImageView = UIImageView(image: nil)
let seatLabel: CustomLabel = {
let label = CustomLabel()
return label
}()
// MARK: - Lifecycle
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configure() {
guard let viewModel = viewModel else { return }
seatLabel.text = viewModel.text
seatLabel.backgroundColor = .cyan
seatLabel.sizeToFit()
seatImageView.image = .strokedCheckmark
let stackView = UIStackView(arrangedSubviews: [seatImageView, seatLabel])
stackView.spacing = 0
stackView.alignment = .center
addSubview(stackView)
seatImageView.snp.makeConstraints { make in
make.size.equalTo(40)
}
stackView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
}
// MARK: - CUSTOM TITLE LABEL
class CustomLabel: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
self.textColor = .darkGray
self.textAlignment = .left
self.numberOfLines = 4
}
}
Assuming you want all of the cells to be the same size...
One possible solution would be to use NSString.boundingRect. In a place where you only need to calculate this once, for example in viewDidLoad, determine the bounding rect of the largest text:
var maxTextWidth: CGFloat = 0
override func viewDidLoad() {
// All the other code you currently have, plus:
let boundingWidth = 9999 // Something arbitrarily large
let boundingHeight = 34 // Make this the height of however many lines you want
let targetSize = CGSize(width: boundingWidth, height: boundingHeight)
let attributes = [NSAttributedString.Key.font : UIFont.boldSystemFont(ofSize: 17)] // Put any other necessary attributes to accurately calculate the size of the text
for vm in viewModel {
let width = NSString(string: vm.text).boundingRect(with: estimatedSize, options: .usesLineFragmentOrigin, attributes: attributes, context: nil).width
if width > maxTextWidth {
maxTextWidthWidth = width
}
}
}
And then you also need to provide this size to the collection view using:
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = maxTextWidth + // Any padding you want
let height = // Height of text plus any padding
return CGSize(width: width, height: height)
}
More documentation on boundingRect can be found here
This is my code
class DescriptionsViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
let layout = TagFlowLayout()
layout.estimatedItemSize = CGSize(width: 140, height: 40)
collectionView.collectionViewLayout = layout
}
}
class Row {
var attributes = [UICollectionViewLayoutAttributes]()
var spacing: CGFloat = 0
init(spacing: CGFloat) {
self.spacing = spacing
}
func add(attribute: UICollectionViewLayoutAttributes) {
attributes.append(attribute)
}
func tagLayout(collectionViewWidth: CGFloat) {
let padding = 10
var offset = padding
for attribute in attributes {
attribute.frame.origin.x = CGFloat(offset)
offset += Int(attribute.frame.width + spacing)
}
}
}
class TagFlowLayout: UICollectionViewFlowLayout {
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else {
return nil
}
var rows = [Row]()
var currentRowY: CGFloat = -1
for attribute in attributes {
if currentRowY != attribute.frame.origin.y {
currentRowY = attribute.frame.origin.y
rows.append(Row(spacing: 10))
}
rows.last?.add(attribute: attribute)
}
rows.forEach {
$0.tagLayout(collectionViewWidth: collectionView?.frame.width ?? 0)
}
return rows.flatMap { $0.attributes }
}
}
extension DescriptionsViewController : UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return sourse.Ingredients.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "collectionViewCell",for: indexPath) as? collectionViewCell else {
return collectionViewCell()
}
cell.label.text = sourse.Ingredients[indexPath.row] //[indexPath.section]
cell.label.preferredMaxLayoutWidth = collectionView.frame.width - 16
cell.label.sizeToFit()
return cell
}
} // uicollectionViewDatasourse,UICollectionViewDelegate
class collectionViewCell: UICollectionViewCell{
#IBOutlet weak var label: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
self.layer.cornerRadius = label.frame.size.height / 2.0
self.backgroundColor = #colorLiteral(red: 0.3647058904, green: 0.06666667014, blue: 0.9686274529, alpha: 1)
}
} //UICollectionViewCell
The text will beyond the background color and the background color can't adaptation the text length
I added some code and answered my question.
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: sourse.Ingredients[indexPath.item].size(withAttributes: [NSAttributedString.Key.font : UIFont.systemFont(ofSize: 17)]).width + 25, height: 30)
}
But I think it's not best answer because this source code can detection they just add .width + 25.
Obviously this code is not "dynamic", but it does work.
The problem exisit in your Xib constraints. When your label cannot get the right frame, sizeToFit will not work.
If you want the label adapt to your text length, you can try.
Snapkit
view.contentView.addSubview(label)
label.snp.makeConstraints {
$0.centerY.equalToSuperview()
$0.leading.trailing.equalToSuperview().inset(customOffset)
}
Xib
Juset set the same constraints, ___centeY, leading, trailing___, as by SnapKit(I am not familiar how to in Xib, so sorry about no cases)
Original Code
this may help
class tageCollectionViewCell: UICollectionViewCell{
var label: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
setupView()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupView() {
label = UILabel()
//... add your custom character
contentView.addSubview(label)
label.translatesAutoresizingMaskIntoConstraints = false
let constraints = [
label.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor)
]
NSLayoutConstraint.activate(constraints)
}
}
See the following boarder of the each items (Border Spacing)
With collection view Header I am able to achieve the following out put but stuck at how to put separator inside the uicollection view. also the number of cell inside the row is dynamic.
and last row should not the the bottom separator
any help is really appreciate..
For Achieving the following layout i only use the collection view with section header
I have already done the following output
All section are collapsed
clicked on particular secttion
Just the separator part is remaining for each expanded section
I cant figure out how can i achieve the same using decoration view.
You could do it like this,
Create two different types of decoration view, one for vertical line which is at the center of the collection view and other being horizontal which appear below two cell
Vertical decoration view is created only once for the whole view, while horizontal decoration view is created for a pair of two which appears below each row. For the last row, dont create the decoration view.
subclass UICollectionViewFlowLayout, and override override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? and return appropriate decoration view.
Here is how the layout looks for me,
And here is the code used for that,
CollectionViewController,
class ViewController: UICollectionViewController {
let images = ["Apple", "Banana", "Grapes", "Mango", "Orange", "Strawberry"]
init() {
let collectionViewLayout = DecoratedFlowLayout()
collectionViewLayout.register(HorizontalLineDecorationView.self,
forDecorationViewOfKind: HorizontalLineDecorationView.decorationViewKind)
collectionViewLayout.register(VerticalLineDecorationView.self,
forDecorationViewOfKind: VerticalLineDecorationView.decorationViewKind)
super.init(collectionViewLayout: collectionViewLayout)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView?.backgroundColor = UIColor.white
collectionView?.register(CollectionViewCell.self,
forCellWithReuseIdentifier: CollectionViewCell.CellIdentifier)
}
}
extension ViewController: UICollectionViewDelegateFlowLayout {
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return images.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: CollectionViewCell.CellIdentifier,
for: indexPath) as! CollectionViewCell
let name = images[indexPath.item]
cell.imageView.image = UIImage(named: "\(name).png")
cell.label.text = name
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return .zero
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 10 + DecoratedFlowLayout.horizontalLineWidth
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = (collectionView.bounds.size.width - DecoratedFlowLayout.verticalLineWidth) * 0.5
return CGSize(width: width,
height: width + 20)
}
}
CollectionViewCell,
class CollectionViewCell: UICollectionViewCell {
static let CellIdentifier = "CollectionViewCellIdentifier"
var imageView: UIImageView!
var label: UILabel!
override init(frame: CGRect) {
super.init(frame: frame)
createViews()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func createViews() {
imageView = UIImageView(frame: .zero)
imageView.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(imageView)
label = UILabel(frame: .zero)
label.font = UIFont.systemFont(ofSize: 20)
label.textAlignment = .center
label.translatesAutoresizingMaskIntoConstraints = false
contentView.addSubview(label)
NSLayoutConstraint.activate([
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
imageView.leftAnchor.constraint(equalTo: contentView.leftAnchor),
imageView.rightAnchor.constraint(equalTo: contentView.rightAnchor),
label.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
label.leftAnchor.constraint(equalTo: contentView.leftAnchor),
label.rightAnchor.constraint(equalTo: contentView.rightAnchor),
label.topAnchor.constraint(equalTo: imageView.bottomAnchor),
label.heightAnchor.constraint(equalToConstant: 20)
])
}
}
DecoratedFlowLayout,
class DecoratedFlowLayout: UICollectionViewFlowLayout {
static let verticalLineWidth: CGFloat = 20
static let horizontalLineWidth: CGFloat = 20
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
minimumLineSpacing = 40 // should be equal to or greater than horizontalLineWidth
}
override init() {
super.init()
minimumLineSpacing = 40
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let attributes = super.layoutAttributesForElements(in: rect) else {
return nil
}
var attributesCopy: [UICollectionViewLayoutAttributes] = []
for attribute in attributes {
attributesCopy += [attribute]
let indexPath = attribute.indexPath
if collectionView!.numberOfItems(inSection: indexPath.section) == 0 {
continue
}
let firstCell = IndexPath(item: 0,
section: indexPath.section)
let lastCell = IndexPath(item: collectionView!.numberOfItems(inSection: indexPath.section) - 1,
section: indexPath.section)
if let attributeForFirstItem = layoutAttributesForItem(at: firstCell),
let attributeForLastItem = layoutAttributesForItem(at: lastCell) {
let verticalLineDecorationView = UICollectionViewLayoutAttributes(forDecorationViewOfKind: VerticalLineDecorationView.decorationViewKind,
with: IndexPath(item: 0, section: indexPath.section))
let firstFrame = attributeForFirstItem.frame
let lastFrame = attributeForLastItem.frame
let frame = CGRect(x: collectionView!.bounds.midX - DecoratedFlowLayout.verticalLineWidth * 0.5,
y: firstFrame.minY,
width: DecoratedFlowLayout.verticalLineWidth,
height: lastFrame.maxY - firstFrame.minY)
verticalLineDecorationView.frame = frame
attributesCopy += [verticalLineDecorationView]
}
let contains = attributesCopy.contains { layoutAttribute in
layoutAttribute.indexPath == indexPath
&& layoutAttribute.representedElementKind == HorizontalLineDecorationView.decorationViewKind
}
let numberOfItemsInSection = collectionView!.numberOfItems(inSection: indexPath.section)
if indexPath.item % 2 == 0 && !contains && indexPath.item < numberOfItemsInSection - 2 {
let horizontalAttribute = UICollectionViewLayoutAttributes(forDecorationViewOfKind: HorizontalLineDecorationView.decorationViewKind,
with: indexPath)
let frame = CGRect(x: attribute.frame.minX,
y: attribute.frame.maxY + (minimumLineSpacing - DecoratedFlowLayout.horizontalLineWidth) * 0.5,
width: collectionView!.bounds.width,
height: DecoratedFlowLayout.horizontalLineWidth)
horizontalAttribute.frame = frame
attributesCopy += [horizontalAttribute]
}
}
return attributesCopy
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
}
And decoration views
class VerticalLineDecorationView: UICollectionReusableView {
static let decorationViewKind = "VerticalLineDecorationView"
let verticalInset: CGFloat = 40
let lineWidth: CGFloat = 4.0
let lineView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
lineView.backgroundColor = .black
addSubview(lineView)
lineView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
lineView.widthAnchor.constraint(equalToConstant: lineWidth),
lineView.topAnchor.constraint(equalTo: topAnchor, constant: verticalInset),
lineView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -verticalInset),
lineView.centerXAnchor.constraint(equalTo: centerXAnchor),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class HorizontalLineDecorationView: UICollectionReusableView {
let horizontalInset: CGFloat = 20
let lineWidth: CGFloat = 4.0
static let decorationViewKind = "HorizontalLineDecorationView"
let lineView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
lineView.backgroundColor = .black
addSubview(lineView)
lineView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
lineView.heightAnchor.constraint(equalToConstant: lineWidth),
lineView.leftAnchor.constraint(equalTo: leftAnchor, constant: horizontalInset),
lineView.rightAnchor.constraint(equalTo: rightAnchor, constant: -horizontalInset),
lineView.centerYAnchor.constraint(equalTo: centerYAnchor),
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I hope you can make use of it and tweak values to suit your own need. Some of the calculations might make sense to change to some level. But, I hope you get the idea about how this could be achieved.
If the number of columns is always 2, why don't you add three views(separators) to a cell, two on the sides and one on the bottom. For the cells on the right side, hide the right most separator and vice versa for cells on the left.
Hide the separator on the bottom for cells on the last row.
It is a bit of a hack but much simpler to implement.
customize UICollectionviewcell (I think you might already have done that),
Now on custom UICollectionviewcell put two UIView -
one UIView at the right of the cell with 1 or 2 point width(as per
required) and height equals to cell height, give a black background
color to UIView.
Another UIView at the bottom of the cell with 1 or 2 point height(as per required) and this time width equals to cell width, give a black background color to UIView.
and adjust the spaces.
I thing this trick will work for you.
I am trying to create a stretchy header using a collectionview section header. There are a bunch of different ways to do this but I am just looking for the simplest and most straight forward method with clear insturctions on what to do. Has anybody seen a simple guide on doing this in Swift 3 or can you explain how this would be done here?
I don't think it should be that difficult. I would like to use the UICollectionViewDelegateFlowLayout and the ScrollviewDelegate becuase I think that would be the easiest way for it to be done.
Can I use the scrollViewDidScroll method to control height of the header as a user is scrolling. How would I change the height of the header manually? I know on the storyboard I can change it in the UICollectionView header size settings but how would I adjust it in code?
With Swift 5 / iOS 12.3, you can override shouldInvalidateLayout(forBoundsChange:) and layoutAttributesForElements(in:) methods inside a UICollectionViewFlowLayout subclass in order to create a stretchy header in a UICollectionView. The following sample code shows how to implement those methods:
CollectionViewController.swift
import UIKit
class CollectionViewController: UICollectionViewController {
let flowLayout = CustomFlowLayout()
override func viewDidLoad() {
super.viewDidLoad()
guard let collectionView = collectionView else { fatalError() }
collectionView.alwaysBounceVertical = true
collectionView.contentInsetAdjustmentBehavior = .always
collectionView.collectionViewLayout = flowLayout
collectionView.register(CollectionViewCell.self, forCellWithReuseIdentifier: "Cell")
collectionView.register(HeaderReusableView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView")
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "HeaderView", for: indexPath) as! HeaderReusableView
return headerView
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 20
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! CollectionViewCell
}
}
CustomFlowLayout.swift
import UIKit
class CustomFlowLayout: UICollectionViewFlowLayout {
let idealCellWidth: CGFloat = 100
let margin: CGFloat = 10
override init() {
super.init()
sectionInsetReference = .fromSafeArea
sectionInset = UIEdgeInsets(top: margin, left: margin, bottom: margin, right: margin)
headerReferenceSize = CGSize(width: 0, height: 80)
sectionHeadersPinToVisibleBounds = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func prepare() {
super.prepare()
guard let collectionView = collectionView else { return }
let availableWidth = collectionView.frame.width - collectionView.safeAreaInsets.left - collectionView.safeAreaInsets.right - sectionInset.left - sectionInset.right
let idealNumberOfCells = (availableWidth + minimumInteritemSpacing) / (idealCellWidth + minimumInteritemSpacing)
let numberOfCells = idealNumberOfCells.rounded(.down)
let cellWidth = (availableWidth + minimumInteritemSpacing) / numberOfCells - minimumInteritemSpacing
itemSize = CGSize(width: cellWidth, height: cellWidth)
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
guard let collectionView = collectionView else { return nil }
guard let rectAttributes = super.layoutAttributesForElements(in: rect) else { return nil }
let offsetY = collectionView.contentOffset.y + collectionView.safeAreaInsets.top
if let firstHeader = rectAttributes.first(where: { $0.representedElementKind == UICollectionView.elementKindSectionHeader && offsetY < 0}) {
let origin = CGPoint(x: firstHeader.frame.origin.x, y: firstHeader.frame.minY - offsetY.magnitude)
let size = CGSize(width: firstHeader.frame.width, height: max(0, headerReferenceSize.height + offsetY.magnitude))
firstHeader.frame = CGRect(origin: origin, size: size)
}
return rectAttributes
}
}
CollectionViewCell.swift
import UIKit
class CollectionViewCell: UICollectionViewCell {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .cyan
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
HeaderReusableView.swift
import UIKit
class HeaderReusableView: UICollectionReusableView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .magenta
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Expected result:
I've got a custom subclass of UICollectionViewFlowLayout, and I'm trying to update the cell size to always be the width of the screen. The cells aren't updating properly when I rotate to landscape. Here's my subclass:
// MARK: - Constants
private let EdgeInset: CGFloat = 10
private let CellSpacing: CGFloat = 10
private let LineSpacing: CGFloat = 10
private let CellHeight: CGFloat = 50
// MARK: - FullWidthLayout
class FullWidthLayout: UICollectionViewFlowLayout {
override init() {
super.init()
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() {
minimumLineSpacing = LineSpacing
minimumInteritemSpacing = CellSpacing
sectionInset = UIEdgeInsets(top: EdgeInset, left: EdgeInset, bottom: EdgeInset, right: EdgeInset)
}
}
// MARK: Layout Methods
extension FullWidthLayout {
override func prepare() {
super.prepare()
if let view = collectionView {
let availableWidth = view.bounds.width - EdgeInset * 2
itemSize = CGSize(width: availableWidth, height: CellHeight)
print("Item size: \(itemSize)")
}
}
}
Here's the output in the console:
Item size: (300.0, 50.0)
Item size: (548.0, 50.0)
And here's what's happening in the simulator:
The item size seems to be set correctly in the prepare() method, but the cell doesn't update in the collection view. Exceptions are thrown if you start in landscape and move to portrait.
Is there something I'm missing? Is the base UICollectionViewFlowLayout ignoring the itemSize in some cases?
Here's a link to the project on GitHub: CollectionViewRotate
Add this in your ViewController
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.size.width/ 2 - 10, height: 100) // chnage as per your need ...10 is the space between cell
}
override func didRotateFromInterfaceOrientation(fromInterfaceOrientation: UIInterfaceOrientation) {
myCollectionView!.reloadData()
}