I'm adding shadow to a cell, however when I start using autolayout to set objects within the cell it negatively effects the shape of the cell. I've tried various stages of views/nib loading, but can't seem to get the combo right. What can I do to allow for using autolayout within the cell?
Desired (no autolayout):
Result (after setting the label to "equal width" and distance from bottom):
From the viewcontroller:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PlacesCell", for: indexPath) as! PlacesCollectionViewCell
let shadowPath2 = UIBezierPath(rect: cell.bounds)
cell.layer.cornerRadius = 10
cell.layer.masksToBounds = false
cell.layer.shadowColor = UIColor.black.cgColor
cell.layer.shadowOffset = CGSize(width: CGFloat(2.0), height: CGFloat(6.0))
cell.layer.shadowOpacity = 0.3
cell.layer.shadowPath = shadowPath2.cgPath
cell.name.text = someObjects[indexPath.row].name
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let noOfCellsInRow = 2
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let totalSpace = flowLayout.sectionInset.left
+ flowLayout.sectionInset.right
+ (flowLayout.minimumInteritemSpacing * CGFloat(noOfCellsInRow - 1))
let size = Int((collectionView.bounds.width - totalSpace) / CGFloat(noOfCellsInRow))
return CGSize(width: size, height: size)
}
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if let cell = sender as? UICollectionViewCell,
let indexPath = self.placesCollectionView.indexPath(for: cell) {
let vc = segue.destination as! PeopleViewController
vc.placeForReference = someObjects[indexPath.row] //Pass object
}
}
The cell's class only inherits from UICollectionViewCell.
Figured it out; in the collectionview's settings I needed to change the "Estimated Size" to "None".
Related
I am using collectionview in tableview cell and showing collectionview height according to number of cells from JSON response according to this answer
the code:
class ViewProposalTableVIewCell: UITableViewCell, UICollectionViewDelegate,UICollectionViewDataSource {
#IBOutlet weak var attCollHeight: NSLayoutConstraint!
#IBOutlet weak var attetchmentsCollectionview: UICollectionView!
override func awakeFromNib() {
super.awakeFromNib()
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .vertical
let width = UIScreen.main.bounds.width/2.5
let height = CGFloat(40)
layout.minimumInteritemSpacing = 0
layout.minimumLineSpacing = 0
layout.itemSize = CGSize(width: width, height: height)
self.attetchmentsCollectionview.collectionViewLayout = layout
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ViewProposalTableVIewCell", for: indexPath) as! ViewProposalTableVIewCell
let bidData = viewproposalData?.result?.bids?[indexPath.row]
cell.propAmt.text = "$\(bidData?.amount ?? "")"
cell.frame = tableView.bounds
cell.layoutIfNeeded()
cell.attetchmentsCollectionview.reloadData()
cell.attCollHeight.constant = cell.attetchmentsCollectionview.collectionViewLayout.collectionViewContentSize.height;
cell.attetchmentsCollectionview.reloadData()
return cell
}
then the 0/p: but i don't want the gap between the cells how to make both cells come close
EDIT: according to below answer and removed code in awakeFromNib then the o/p
where am i wrong
Set this settings for collectionview from storybord
Add this in code
extension homeVC: UICollectionViewDelegateFlowLayout,
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let noOfCellsInRow = 2
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let totalSpace = flowLayout.sectionInset.left
+ flowLayout.sectionInset.right
+ (flowLayout.minimumInteritemSpacing * CGFloat(noOfCellsInRow - 1))
let size = Int((collectionView.bounds.width - 5) / CGFloat(noOfCellsInRow))
return CGSize(width: size, height: 20)
}
}
You need to set insets
func collectionView(collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAtIndex section: Int) -> UIEdgeInsets {
let cellWidth = 50
let noOfCellsInRow = 2
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let space = flowLayout.minimumInteritemSpacing
let totalCellWidth = cellWidth * noOfCellsInRow
let leftInset = (collectionView.frame.width - (CGFloat(totalCellWidth) + space)) / 2
let rightInset = leftInset
return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)
}
I have the following code:
import UIKit
import Photos
import PhotosUI
private let reuseIdentifier = "Cell"
class CollectionVC: UICollectionViewController, UICollectionViewDelegateFlowLayout {
var imageArray = [UIImage]()
override func viewDidLoad() {
super.viewDidLoad()
grapPhotos()
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return imageArray.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath as IndexPath)
let imageView = cell.viewWithTag(1) as! UIImageView
collectionView.allowsMultipleSelection = true
cell.layer.cornerRadius = 4
imageView.image = imageArray[indexPath.row]
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) {
cell.layer.borderColor = #colorLiteral(red: 0.1411764771, green: 0.3960784376, blue: 0.5647059083, alpha: 1)
cell.layer.borderWidth = 5
}
}
override func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
if let cell = collectionView.cellForItem(at: indexPath) {
cell.layer.borderColor = #colorLiteral(red: 0.1411764771, green: 0.3960784376, blue: 0.5647059083, alpha: 1)
cell.layer.borderWidth = 0
}
}
func grapPhotos() {
let imgManager = PHImageManager.default()
let requestOptions = PHImageRequestOptions()
requestOptions.isSynchronous = true
requestOptions.deliveryMode = .highQualityFormat
let fetchOptions = PHFetchOptions()
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
fetchOptions.predicate = NSPredicate(format: "mediaType = %d || mediaType = %d", PHAssetMediaType.image.rawValue, PHAssetMediaType.video.rawValue)
if let fetchResult : PHFetchResult = PHAsset.fetchAssets(with: fetchOptions) {
if fetchResult.count > 0 {
for i in 0..<fetchResult.count {
imgManager.requestImage(for: fetchResult.object(at: i), targetSize: CGSize(width: 200, height: 200), contentMode: .aspectFill, options: requestOptions, resultHandler: {
image, error in
self.imageArray.append(image!)
})
}
}
else {
self.collectionView?.reloadData()
print("No Photos")
}
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let width = collectionView.frame.width / 3 - 6
return CGSize(width: width, height: width)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumLineSpacingForSectionAt section: Int) -> CGFloat {
return 6.0
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
return 6.0
}
}
I apologize it's a lot but I really have no idea where I am going wrong. This is all my code for my CollectionViewController, and then in storyboard I have a collection VC with an image in the one cell with a tag as 1 and then the one cell with an identifier as "Cell". The problem I am having is when I run the code on my phone, selecting one image ends up selecting others. For example: I select the first image, then I scroll down and another image is already selected. I did a little test and the first 21 images are almost unique. But then it's as if the image almost have the same id. Like selecting the first image also selects the 22nd image. By the way I am building a custom image picker.
There's more than one issue here, but the main problem you're complaining of is because you're forgetting that cells are reused. You need to configure cells in cellForItem, not in didSelect and didDeselect.
When your didSelect turns around and talks directly to a physical cell, it's doing totally the wrong thing. Instead, didSelect and didDeselect should talk to your data model, setting some property in the model's objects that says, the objects at these index paths should be considered selected, and the objects at these other index paths should not. Then reload the data.
Then, when cellForItem comes along, it must configure its cell completely, either displaying its selected appearance or its unselected appearance, based on the data model and index path. That way, if a cell was in a previous selected row but is now reused in an unselected row, it has the desired unselected appearance.
In other words, only cellForItem should think in terms of cells. Everyone else should think solely in terms of index path (or even better, in terms of some unique identifier associated with the data, but that's another story).
I have a UIcollectionView with images. When I rotate the screen, the cells do not resize as expected. I have tried adding collectionView.collectionViewLayout.invalidateLayout() to the viewDidLayoutSubviews() - but this only causes a crash. Please can someone advise?
I have a UIcollectionView with images. When I rotate the screen, the cells do not resize as expected. I have tried adding collectionView.collectionViewLayout.invalidateLayout() to the viewDidLayoutSubviews() - but this only causes a crash. Please can someone advise?
Portrait:
Landdscape:
class GridPicksCollectionViewController: UICollectionViewController{
let cellId = "gridyPickCell"
var numCelPerRow: CGFloat = 3
var layout: UICollectionViewFlowLayout!
let borderInset: CGFloat = 3
let interCellSpacing: CGFloat = 3
let spacingBetweenRows: CGFloat = 3
var dataSource = [UIImage]()
var collectionViewWidth: CGFloat!
override func viewDidLoad() {
super.viewDidLoad()
generalSetup()
setUpDateSource()
setupCollectionViewLayout()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
print("viewDidLayoutSubviews")
// Update collectionViewWidth upon rotation
collectionViewWidth = self.collectionView.frame.width
//collectionView.collectionViewLayout.invalidateLayout() - causes app to crash
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
print("viewWillTransition")
}
private func generalSetup(){
navigationController?.navigationBar.barTintColor = UIColor.appDarkGreen
navigationItem.title = "Girdy Picks"
let textAttributes = [NSAttributedString.Key.foregroundColor:UIColor.white]
navigationController?.navigationBar.titleTextAttributes = textAttributes
collectionView.backgroundColor = UIColor.white
collectionViewWidth = collectionView.frame.width
// Make sure collectionView is always within the safe area layout
if #available(iOS 11.0, *) {
collectionView?.contentInsetAdjustmentBehavior = .always
}
// Register UICollectionViewCell
collectionView.register(GirdyPickCollectionCell.self, forCellWithReuseIdentifier: cellId)
}
private func setUpDateSource(){
let images: [UIImage] = [UIImage.init(named: "buddha")!, UIImage.init(named: "sharpener")!, UIImage.init(named: "cars")!, UIImage.init(named: "houses")!, UIImage.init(named: "houses2")!, UIImage.init(named: "tower1")!, UIImage.init(named: "tower2")!]
dataSource = images
}
private func setupCollectionViewLayout(){
collectionView.delegate = self
collectionView.dataSource = self
layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout
layout.minimumInteritemSpacing = interCellSpacing // distance between cells in a row
layout.minimumLineSpacing = spacingBetweenRows // distance in between rows
layout.sectionInset = UIEdgeInsets.init(top: borderInset, left: borderInset, bottom: borderInset, right: borderInset) // border inset for collectionView
}
}
extension GridPicksCollectionViewController: UICollectionViewDelegateFlowLayout{
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return dataSource.count
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! GirdyPickCollectionCell
cell.imageView.image = dataSource[indexPath.row]
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let selectedImage = dataSource[indexPath.row]
let showImageVC = ImageGridViewController()
showImageVC.modalPresentationStyle = .fullScreen
showImageVC.imageToDisplay = selectedImage
self.present(showImageVC, animated: true) {}
}
// Determine size for UICollectionCell
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
print("sizeForItemAt")
guard let collectionWidth = collectionViewWidth else{ return CGSize.init()}
let widthCell = (collectionWidth - interCellSpacing * 2 - borderInset * 2) / numCelPerRow
return CGSize.init(width: widthCell, height: widthCell)
}
}
You are wrong about your lifecycle of events. When the device is rotated, your view hierarchy becomes notified of this event, and views start to re-layout themselves based on the information they have. After this has been finished, viewDidLayoutSubviews() will be called, not before.
Since you are updating the collectionViewWidth property in this method, the layout still uses the old value when calling collectionView(_:layout:sizeForItemAt:). You need to set this value in viewWillLayoutSubviews().
Alternatively, you can write a setter for this property which will call invalidateLayout() on the collection view's layout, but this will cause an unnecessary layout pass.
Context -
I am working on a macOS menu bar app which displays a collection of items. The items exist in a collection view and contain a textfield.
Right now, the collection view layout is at a fixed height of 50, and I am trying to dynamically increase the size of the cell based on the size of the textfield.
I believe the best way to do this is in the sizeForItemAt method, but I can not seem to reference a cell and its textfield to calculate and return it's height. Whenever I try and get a collectiomView item at this point, the app crashes with an [General] *** -[__NSArrayM objectAtIndex:]: index 1 beyond bounds [0 .. 0] exception.
My Code -
My viewDidLoad, where I 'configure' my CollectionView
override func viewDidLoad() {
super.viewDidLoad()
configureCollectionView()
....
}
private func configureCollectionView() {
let flowLayout = NSCollectionViewFlowLayout()
flowLayout.itemSize.width = self.view.frame.width
flowLayout.minimumLineSpacing = 8
thoughtsCollectionView.collectionViewLayout = flowLayout
view.wantsLayer = true
}
This is where Im trying to set the height dynamically (and where it crashes)
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
guard collectionView.item(at: indexPath) != nil else { // <-- CRASHES HERE
return NSSize(width: self.view.frame.width, height: 50)
}
return self.collectionView.item(at: indexPath)?.textField?.frame.height
}
This is how Im adding items to the collectionView
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
let item = collectionView.makeItem(withIdentifier: "CollectionViewItem", for: indexPath as IndexPath)
guard let collectionViewItem = item as? CollectionViewItem else {return item}
(item as! CollectionViewItem).textField?.stringValue = "Some Text"
(item as! CollectionViewItem).delegate = self
return item
}
So what is strange to me, is that in my collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) I am to successfully get the item by doing
func collectionView(_ collectionView: NSCollectionView, didSelectItemsAt indexPaths: Set<IndexPath>) {
guard let indexPath = indexPaths.first else {
return
}
guard collectionView.item(at: indexPath) != nil else {
return
}
let item = collectionView.item(at: indexPath) as! CollectionViewItem // <--- This gets me an item fine
So I'm unsure why I'm struggling with sizeForItemAt. I have a feeling its because the items don't exist yet (and are created when the view starts to display them), but unsure how to then correctly approach this dynamic sizing.
I encounter the same issue. With reading the documents, I find that Apple intents to let us use the NSCollectionViewLayout instead of the item.
If you don't know how to deal with NSCollectionViewLayout, you can try to use your data model to regenerate the views and calculate.
For example, if you have one image, one mutable size label and one fixed size label. like: Image | Mutable Label | Label
You can regenerate the mutable label like this.
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
let imageViewWidth:CGFloat = 20 + 64 + 20
let account = self.dataObject[indexPath]
let defaultLabelWidth:CGFloat = {
if account.isDefault {
let defaultLabel = NSTextField(labelWithString: NSLocalizedString("Default", comment: ""))
defaultLabel.font = NSFont.systemFont(ofSize: 16.0)
defaultLabel.sizeToFit()
return 20 + defaultLabel.bounds.width + 20
}
return 0 + 0 + 20
}()
let nameLabelWidth:CGFloat = {
let label = NSTextField(labelWithString: account.name)
label.font = NSFont.systemFont(ofSize: 16.0)
label.sizeToFit()
return label.bounds.width
}()
return NSSize(width: imageViewWidth + nameLabelWidth + defaultLabelWidth, height: 80)
}
In my collection view, the cell class must have completely different appearance for different kinds of data in the array.
I am looking for a way to create multiple cells and choose a different one according to the input data, so for example :
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell1 = collectionView.dequeueReusableCell.. as! kind1
let cell2 = collectionView.dequeueReusableCell.. as! kind2
// here choose a different one for each kind of data
return cell1
}
I am trying to understand if :
How to do this and if its the right way in terms of memory ?
Is it a good design? how would you go about making a completely different layout of a single cell? ( creating views and hiding them seems like a bad approach)
You need to do something like this
First register multiple cell -
[collectionView registerNib:[UINib nibWithNibName:#"CollectionViewCellKind1" bundle:nil] forCellWithReuseIdentifier:#"CollectionViewCellKind1"]
[collectionView registerNib:[UINib nibWithNibName:#"CollectionViewCellKind2" bundle:nil] forCellWithReuseIdentifier:#"CollectionViewCellKind2"]
Now implement cellForItemAt like show -
internal func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if (data.kind == kind1) {
let cell1 = collectionView.dequeueReusableCell.. as! kind1
return cell1
} else {
let cell2 = collectionView.dequeueReusableCell.. as! kind2
return cell2
}
}
by checking the type of the data you can determine the cell.
Swift 5 example, change depending on your needs.
class ViewController: UIViewController, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(collectionView)
collectionView.translatesAutoresizingMaskIntoConstraints = false
collectionView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0).isActive = true
collectionView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0).isActive = true
collectionView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0).isActive = true
collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
registerCells()
}
lazy var collectionView: UICollectionView = {
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
cv.backgroundColor = .white
cv.dataSource = self
cv.delegate = self
cv.isPagingEnabled = true
return cv
}()
var cellId = "Cell"
var celltwoCellId = "CellTwoCell"
fileprivate func registerCells() {
collectionView.register(CellOneCell.self, forCellWithReuseIdentifier: cellId)
collectionView.register(CellTwoCell.self, forCellWithReuseIdentifier: celltwoCellId)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if indexPath.item == 0
{
// Cell 1
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellId, for: indexPath) as! CellOne
return cell
}
else
{
// Cell 2
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: celltwoCellId, for: indexPath) as! CellTwo
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: view.frame.width, height: view.frame.height)
}
}
I just want to mention this, for any future users who might wonder how to do that. Now, I think it's much easier to just use the indexPath.item and check whether or not it's cell 1 or cell 2. Here's an example
if indexPath.item == 0
{
// Cell 1
let cell1 = let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell1", for: indexPath) as! Cell1
return cell1
}
else
{
// Cell 2
let cell2 = let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell2", for: indexPath) as! Cell1
return cell2
}
In case you have more than 2 custom UICollectionViewCells then just add else if statements and check against its indexPath.item.