How to determine the size of collectionviewcell (xib) - ios

Is there a way to measure the width and height of a cell defined in xib from within "sizeforitemat" function or any other function of viewcontroller, once data is dynamically generated? The default layout of the collection view makes it very shabby as depicted in output. Principally layout should have maximally two columns, and the rest of the empty spaces should be divided between cells within a row. So I want to loop over all the items, once it is filled with data to determine the maximum size of amongst cells to generate a layout according to its size. I'll highly appreciate the response.
viewcontroller.swift
import UIKit
class ViewController: UIViewController, UICollectionViewDelegate,
UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var collectionView: UICollectionView!
let myCell: String = "CollectionViewCell"
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
self.collectionView.register(UINib(nibName:myCell, bundle: nil), forCellWithReuseIdentifier: myCell)
self.collectionView.delegate = self
self.collectionView.dataSource = self
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
150
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: myCell, for: indexPath) as! CollectionViewCell
cell.lbl1.text = "\(indexPath.row)"
cell.lbl2.text = "\(String(describing: cell.lbl2.text))\(indexPath.row)"
print ("***", indexPath.item, cell.bounds.size, collectionView.frame.size)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
// TODO: measure width and heigth of each cell to find number of columns
return collectionView.frame.size
}
}
collectionviewcell.swift
import UIKit
class CollectionViewCell: UICollectionViewCell {
#IBOutlet weak var lbl1: UILabel!
#IBOutlet weak var lbl2: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
}
viewcontroller's code
xib file's code
collection view cell
output

To find the "widest" cell, you can:
create an instance of your cell
loop through your data, setting the label values
get the width
track the maximum width
Something like this:
let theData: [String] = [
"Short",
"Longer",
"A bit longer",
"ABC",
"This is the longest one",
"Another medium",
"Last one"
]
let nib: UINib = UINib(nibName: "CollectionViewCell", bundle: nil)
let c = nib.instantiate(withOwner: nil, options: nil).first as! CollectionViewCell
var sz: CGSize = .zero
var maxW: CGFloat = 0
for i in 0..<theData.count {
c.lbl1.text = "\(i)"
c.lbl2.text = theData[i]
sz = c.systemLayoutSizeFitting(CGSize(width: .greatestFiniteMagnitude, height: 60.0),
withHorizontalFittingPriority: .defaultLow,
verticalFittingPriority: .required)
maxW = max(maxW, sz.width)
print("Row: \(i)", sz)
}
print("Max Width:", maxW)
Could give this in the debug output:
Row: 0 (50.0, 60.0)
Row: 1 (61.5, 60.0)
Row: 2 (95.5, 60.0)
Row: 3 (42.0, 60.0)
Row: 4 (179.5, 60.0)
Row: 5 (134.5, 60.0)
Row: 6 (73.5, 60.0)
Max Width: 179.5

Related

Loading different heights for cell with UICollectionView

I am trying to dynamically adjust cells in a CollectionView according to the contents of a label. Basically i have found how to but my challenge is
cellForItemAt method
gets called first before
heightForCellAtIndexPath method
and by doing so this means we are trying to get heights before data is loaded so the cells will always be the same height and therefore we wont achieve different heighted cells
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let content_ = content[indexPath.row]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "NotesUI", for: indexPath) as! NotesUI
cell.setData(data: content_)
return cell
}
func collectionView(_ collectionView: UICollectionView, heightForCellAtIndexPath indexPath:IndexPath) -> CGFloat {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "NotesUI", for: indexPath) as! NotesUI
return cell.allNoteViews()[indexPath.item - 1].bounds.size.height
}
Essentially in the NotesUI CollectionView class:
class NotesUI:UICollectionViewCell{
#IBOutlet var note: UILabel!
var views: [UILabel] = []
func setData(data: NoteModel){
note.text = data.note
note.numberOfLines = 0
note.lineBreakMode = NSLineBreakMode.byWordWrapping
note.sizeToFit()
views.append(note)
}
func allNoteViews()->[UILabel]{
return views
}
}
I guess my question boils down to how can i call heightForCellAtIndexPath method after the cellForItemAt method so that the CollectionView gets the height of the label after it has contents
You don't need to calculate each cell height if you use auto layout. You can add a vertical stack view to your cell and add your labels to it.
Here is a basic example of self-sizing collection view cells.
class YourCustomCell: UICollectionViewCell {
// Add your labels to this stackView
#IBOutlet weak var yourLabelContainer: UIStackView!
override func awakeFromNib() {
super.awakeFromNib()
yourLabelContainer.translatesAutoresizingMaskIntoConstraints = false
yourLabelContainer.widthAnchor.constraint(equalToConstant: UIScreen.main.bounds.size.width - 32).isActive = true // for 16 - 16 padding
}
}
class YourViewController: UIViewController {
#IBOutlet weak var yourCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
yourCollectionView.delegate = self
yourCollectionView.dataSource = self
let nib = UINib(nibName: "YourCustomCell", bundle: nil)
yourCollectionView.register(nib, forCellWithReuseIdentifier: "cellReuseId")
if let layout = yourCollectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
}
}
}

How to NOT reuse cells in UICollectionView - Swift

I have UICollectionView in a cell of UITableView. This UICollectionView displays user's profile photos. There are only a few items there (let's assume 10 photos is a maximum).
So, I do not want to reload images from backend every time cell reusing. It's inconvenient and unfriendly UX. Therefore, I want to create 10 cells, load images into them and show.
But I don't know how to create (instead of dequeuing) new custom cells within "cellForItem" method of UICollectionView. Or how to forbid reusing.
I'm a kind of newbie in programming, so I would be glad if you explained me that in simple words and most detailed. Thank you.
Here is my code:
Setting CollectinView (uploading random local photos in testing purpose):
extension PhotosTableViewCell: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.row == 0 {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "AddPhotoCollectionViewCell", for: indexPath)
return cell
} else {
var cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PhotoCollectionViewCell", for: indexPath) as! PhotoCollectionViewCell
cell.photo = UIImage(named: "Photo\(Int.random(in: 1...8))") ?? UIImage(named: "defaultPhoto")
cell.layer.cornerRadius = 10
return cell
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let height = collectionView.frame.height
let width = (height / 16.0) * 10.0
return CGSize(width: width, height: height)
}
}
My custom PhotoCollectionViewCell:
class PhotoCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var imageView: UIImageView!
#IBOutlet weak var heightConstraint: NSLayoutConstraint!
#IBOutlet weak var widthConstraint: NSLayoutConstraint!
var photo: UIImage! {
didSet {
// simulate networking delay
perform(#selector(setImageView), with: nil, afterDelay: Double.random(in: 0.2...1))
// setImageView()
}
}
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
#objc private func setImageView() {
let cellHeight = self.frame.height
let cellWidth = self.frame.width
let cellRatio = cellHeight / cellWidth
let photoRatio = photo.size.height / photo.size.width
var estimatedWidth = cellWidth
var estimatedHeight = cellHeight
if photoRatio > cellRatio {
estimatedWidth = cellWidth
estimatedHeight = cellWidth * photoRatio
} else {
estimatedHeight = cellHeight
estimatedWidth = cellHeight / photoRatio
}
widthConstraint.constant = estimatedWidth
heightConstraint.constant = estimatedHeight
imageView.image = photo
}
override func prepareForReuse() {
imageView.image = nil
}
}
Just create, fill and return new cell object in cellForItemAt instead of dequeueing it, but it's not a qood practice and you shouldn't do that.
You need to create new cell each time and assign the value to it. Although this is very dangerous thing to do and can cause your app to take so much memory over use your app will use more power and make the device slower.
Instead I suggest that you save your data into a Collection and to keep reusing it as you go.

Found nil while setting value of a label inside Collection view cell

Case:
I'm using a collection view inside a UIView, I've connected it as an outlet called 'partsCollectionView'. I've created a cell with identifier 'cell' and a custom class 'SelectionCollectionViewCell' for that cell. Inside the cell, I've got a label called 'cellTitle'.
Error: Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value
I'm facing the error while setting the value of that label inside 'cellForItemAt' Method.
Here are the concerned views:
Cell description,
and, collection View Description
The Cell Class is:
import UIKit
class SelectionCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var cellTitle: UILabel!
}
The class where I'll use the collection view is:
import UIKit
private let reuseIdentifier = "cell"
let array = ["small","Big","Medium","Very big","Toooo big String","small"]
class SelectionCollectionViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var partsCollectionView: UICollectionView!
#IBOutlet weak var instructionsView: UITextView!
#IBOutlet weak var image: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
self.partsCollectionView.register(SelectionCollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return array.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = partsCollectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! SelectionCollectionViewCell
cell.layer.cornerRadius = 10
cell.clipsToBounds = true
cell.cellTitle.text = array[indexPath.row]
cell.layer.borderColor = #colorLiteral(red: 0.07843137255, green: 0.6862745098, blue: 0.9529411765, alpha: 0.6819349315)
cell.layer.borderWidth = 2
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let name = array[indexPath.row]
let attributedName = NSAttributedString(string: name)
let width = attributedName.widthWithConstrainedHeight(height: 20.0)
return CGSize(width: width + 40, height: 30.0)
}
}
extension NSAttributedString {
func widthWithConstrainedHeight(height: CGFloat) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, context: nil)
return boundingBox.width
}
}
Update: This is how it looks if I skip setting the title text. The title will come inside those rounded boxes.
Remove this line from viewDidLoad.
self.partsCollectionView.register(SelectionCollectionViewCell.self, forCellWithReuseIdentifier: reuseIdentifier)
When you register a cell programmatically it creates a new collection view cell object. It doesn't get the cell from storyboard. So cellTitle will be nil
OR
Programmatically initialize the label in custom collection view cell
class SelectionCollectionViewCell:UICollectionViewCell {
let cellTitle = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
addSubview(cellTitle)
cellTitle.frame = CGRect(x: 10, y: 10, width: 100, height: 30)
}
}
Just minor mistake is there I think, In cellForItemAt indexPath Please update following line,
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! SelectionCollectionViewCell
I just paste your code and got following output, nothing else,
If you still unable to produce desired output, just delete existing UILabel from SelectionCollectionViewCell and add it again.
FYI. No need to register cell in viewDidLoad() method

UICollectionViewCell from xib file not showing up in UICollectionView

I have a custom xib file which contains a label and a UICollectionView. I have a second xib file for the collection view's cell with a custom subclass of UICollectionViewCell.
The parent xib file's owner looks like below-
import UIKit
class PackageSizePickerVC: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var collection: UICollectionView!
let sizes: [PackageSize] = {
let small = PackageSize(title: "Small", imageName: "S")
let medium = PackageSize(title: "Medium", imageName: "M")
let large = PackageSize(title: "Large", imageName: "L")
let extralarge = PackageSize(title: "Extra Large", imageName: "XL")
return [small, medium, large, extralarge]
}()
override func viewDidLoad() {
super.viewDidLoad()
collection.delegate = self
collection.dataSource = self
collection.register(UINib(nibName: "SizesCell", bundle: nil), forCellWithReuseIdentifier: "Sizecell") //register with nib
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return sizes.count
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: collectionView.frame.width, height: collectionView.frame.height)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if let cell = collection.dequeueReusableCell(withReuseIdentifier: "Sizecell", for: indexPath) as? SizesCell {
let size = sizes[indexPath.row]
cell.image = size.imageName
cell.title = size.title
return cell
}else{
return UICollectionViewCell()
}
}
}
The UICollectionViewCell xib file is named SizesCell.xib and the class file is SizesCell.swift, the code in SizesCell class looks like this-
import UIKit
class SizesCell: UICollectionViewCell {
#IBOutlet weak var sizeImage: UIImageView!
#IBOutlet weak var sizeLabel: UILabel!
var image: String!
var title: String!
override init(frame: CGRect) {
super.init(frame: frame)
sizeImage.image = UIImage(named: image)
sizeLabel.text = title
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Where PackageSize is a Struct as
struct PackageSize {
let title: String
let imageName: String
}
Now the problem I am facing is that the cells simply won't load into the collection view in the parent xib file and I simply can't figure out why the init in the UICollectionViewCell class is not being called at all. I have tried it with awakeFromNib() as well, but that didn't work either. The file owners, custom classes etc. are all set correctly in the IB. What am I missing here?

How to show collection view 3 columns in 1 row with RxSwift

Here is my code:
import UIKit
import RxSwift
import RxCocoa
import RxOptional
class ViewController: UIViewController {
#IBOutlet weak var collectionView: UICollectionView!
let disposeBag = DisposeBag()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
let items = Observable.just(
(0..<20).map{ "Test \($0)" }
)
items.asObservable().bindTo(self.collectionView.rx.items(cellIdentifier: CustomCollectionViewCell.reuseIdentifier, cellType: CustomCollectionViewCell.self)) { row, data, cell in
cell.data = data
}.addDisposableTo(disposeBag)
}
}
So how to show 3 columns in 1 row.
I can't find any tutorial about collection view with RxSwift.
Something like this:
class ViewController: UIViewController, UICollectionViewDelegateFlowLayout {
....
override func viewDidLoad() {
super.viewDidLoad()
...
items.asObservable().bindTo(self.collectionView.rx.items(cellIdentifier: CustomCollectionViewCell.reuseIdentifier, cellType: CustomCollectionViewCell.self)) { row, data, cell in
cell.data = data
}.addDisposableTo(disposeBag)
// add this line you can provide the cell size from delegate method
collectionView.rx.setDelegate(self).addDisposableTo(disposeBag)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.bounds.width
let cellWidth = (width - 30) / 3 // compute your cell width
return CGSize(width: cellWidth, height: cellWidth / 0.6)
}
}
Another alternative:
let flowLayout = UICollectionViewFlowLayout()
let size = (collectionView.frame.size.width - CGFloat(30)) / CGFloat(3)
flowLayout.itemSize = CGSize(width: size, height: size)
collectionView.setCollectionViewLayout(flowLayout, animated: true)

Resources