I have the following code, and the orientation works well when it loads but when the orientation is changed it gets distorted, that is if it loads in portrait and change to landscape, the corner radius gets distorted, like it changes from circle to square.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = categoryCV.dequeueReusableCell(withReuseIdentifier: "CategoryCVC", for: indexPath) as! CategoryCollectionViewCell
cell.catLabel.text = cities[indexPath.row].name
cell.catImg.image = UIImage(named:cities[indexPath.row].image)
cell.layer.cornerRadius = cell.layer.frame.size.height/2
cell.layer.borderColor = UIColor.white.cgColor
cell.layer.borderWidth = 2
return cell
}
How to avoid this from happening? Is there a way to call cellForItemAt indexpath in viewDidLayoutSubviews() and then configuring corner radius there ?
Can you try
func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)
{
self.collectionView.reloadData()
self.collectionView.layoutIfNeeded()
}
Related
I have been trying to fill in a UIImageView with a UIImage (car system image). I have tried numerous solutions online but can not seem to have it fill the UICollectionViewCell, awkward spacing seen here:
I figured since the UIView is the size of the UICollectionViewCell as seen here:
It must be the image is not scaling to the view. So I modified the content mode to scale to fill of the image view when creating the cells for the collection view, then adding this image view into the collection view cell:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CarCell", for: indexPath)
let carImageView = UIImageView(image: UIImage(systemName: "car"))
carImageView.contentMode = .scaleToFill
cell.addSubview(carImageView)
return cell
}
This doesn't seem to have any effect (even when using .bottom for the content mode the image does not move), but looking in the view hierarchy it seems this is what should be fixing it.
Does anyone know what I may be doing wrong?
Here is the storyboard:
Update:
I have added constraints to the image view with this code:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CarCell", for: indexPath)
let carImage = UIImage(systemName: "car")
let carImageView = UIImageView(image: carImage)
carImageView.contentMode = .scaleToFill
cell.addSubview(carImageView)
carImageView.topAnchor.constraint(equalTo: cell.topAnchor).isActive = true
carImageView.bottomAnchor.constraint(equalTo: cell.bottomAnchor).isActive = true
carImageView.leftAnchor.constraint(equalTo: cell.leftAnchor).isActive = true
carImageView.rightAnchor.constraint(equalTo: cell.rightAnchor).isActive = true
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let columns: CGFloat = 2
let collectionViewWidth = collectionView.bounds.width
let flowLayout = collectionViewLayout as! UICollectionViewFlowLayout
let spaceBetweenCells = flowLayout.minimumInteritemSpacing * (columns - 1)
let adjustedWidth = collectionViewWidth - spaceBetweenCells
let width: CGFloat = floor(adjustedWidth / columns)
let height: CGFloat = width
return CGSize(width: width, height: height)
}
But it ends up shrinking the image view rather than growing it to the size of the cell:
Give constraint to your imageView is zero from all side of collectionView cell and set image content mode to .scaleToFill and than show the result.
Thanks
I got you there. As I previously commented to show how you are giving constraints to image view, thus the error was there. There is a property called "translatesautoresizingmaskintoconstraints" which you need set while giving constraints to your views. You can read about it here.
Now, in your cellForItemAt you need to change:-
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CarCell", for: indexPath)
let carImage = UIImage(systemName: "car")
let carImageView = UIImageView(image: carImage)
carImageView.contentMode = .scaleToFill
carImageView.translatesautoresizingmaskintoconstraints = false
cell.addSubview(carImageView)
carImageView.centerXAnchor.constraint(equalTo: cell.contentView.centerXAnchor).isActive = true
carImageView.centerYAnchor.constraint(equalTo: cell.contentView.centerYAnchor).isActive = true
carImageView.heightAnchor.constraint(equalTo: cell.contentView.heightAnchor).isActive = true
carImageView.widthAnchor.constraint(equalTo: cell.contentView.widthAnchor).isActive = true
return cell
}
As you can notice I've also changed the constraint to give an idea about how you should give the constraints if you want to set the image size as exact as your cell. You should always access cell's content view when you're giving constraint not the cell.
I have an app with a screen that is divided into four equal cells. Each cell has an image, label, and color. I'm trying to add the images in, but for some reason, only the image in the first cell works.
It looks like this now:
Here is my code:
In ViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
colorArray += [UIColor.red, UIColor.blue, UIColor.green, UIColor.yellow]
pictureArray += [UIImage(named: "budget")!, UIImage(named: "journey")!,
UIImage(named: "learn")!, UIImage(named: "settings")!]
titleArray += ["Budget", "Journey", "Learn", "Settings"]
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 4
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as UICollectionViewCell
// Set cell properties
cell.backgroundColor = colorArray[indexPath.row]
let imageview:UIImageView=UIImageView(image: pictureArray[indexPath.row])
let size = CGSize(width: 100, height: 100)
imageview.center = cell.center
imageview.bounds = CGRect(origin: cell.bounds.origin, size: size)
cell.contentView.addSubview(imageview)
let label = cell.viewWithTag(1) as! UILabel
label.text = titleArray[indexPath.row]
return cell
}
The labels and colors work fine, but for some reason, the image views seem to be off the screen. I have a feeling that my center/bounds restrictions are forcing the images off the screen, but I've tried many combinations, and none of them seem to work for me.
How should I do this?
I'm posting the correct answer for who ever will google here. (And because it was solved an hour ago without anyone posting the answer)
The best practice is to subclass UICollectionViewCell, connect all the outlets (label, image etc.) and work with this class on the cells
Good luck to all
This is what worked, after Yogesh Suthar's comment:
I subclassed UICollectionViewCell, connected the image and label:
class NavigationCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var navImage: UIImageView!
#IBOutlet weak var navLabel: UILabel!
}
Then, in the ViewController, I did this:
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath) as! NavigationCollectionViewCell
// Set cell properties
cell.backgroundColor = colorArray[indexPath.row]
cell.navImage.image = imageArray[indexPath.row]
cell.navLabel.text = titleArray[indexPath.row]
return cell
}
Set the size and position of the image and label in Main.storyboard!
insert the desired view as a subclass and setup all constrains to cell.heightAchor and so on. this will fix the bug for some reason. It works if the subview is added to the cell as a subview but not as an attribute of the cell
I am using didSelectItemAt and didDeselectItemAt for multiple selection of collectionViewCell. I want to select the cell and make the border blue color if it is selected and also unselect the 'selected' cell and make the border default. But my problem is that didDeselectItemAt is getting called alternately. when once i tap on any cell then didSelectItemAt is called and if i tap on any other cell then didDeselectItemAt is called. This should not happen i guess. didDeselectItemAt should be called only if i am tapping on already selected cell. Please correct me if i am going wrong. I have refered this UICollectionView - didDeselectItemAtIndexPath not called if cell is selected1 but dint work for me :(
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
let cell = collectionView.cellForItem(at: indexPath) as! MomentDetailCell
let moment = self.arrOfMoments[indexPath.row] as! MomentModel
cell.toggleSelection(moment: moment)
self.arrOfDeletingImgs.append(moment)
}
public func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath)
{
let cell : MomentDetailCell = self.collectionViewImages.cellForItem(at: indexPath) as! MomentDetailCell
let moment = self.arrOfMoments[indexPath.row] as! MomentModel
cell.toggleSelection(moment: moment)
self.arrOfDeletingImgs.remove(at: (find(objecToFind: moment))!)
}
// Also this is the code i am using in the class. I have also made allowsMultipleSelection true in viewdidload
extension MomentDetailViewController : UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: 75, height: 75)
}
}
// This is my customCell code
func toggleSelection( moment : MomentModel)
{
if (isSelected)
{
moment.isSelected = true
self.layer.borderWidth = 3
self.layer.borderColor = Constant.APP_BLUE_COLOR.cgColor
}
else
{
moment.isSelected = false
self.layer.borderWidth = 1
self.layer.borderColor = UIColor.red.cgColor
}
}
Try this :
This solution for multiple selection
1- make deSelect as below
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
let cell = collectionView.cellForItem(at: indexPath) as! MomentDetailCell
let moment = self.arrOfMoments[indexPath.row] as! MomentModel
cell.toggleSelection(moment: moment)
self.arrOfDeletingImgs.append(moment)
}
2 - Comment/Remove Deselect method
3 change in this method
func toggleSelection( moment : MomentModel)
{
moment.isSelected = !moment.isSelected
self.layer.borderWidth = moment.isSelected ? 3 : 1
self.layer.borderColor = moment.isSelected ? Constant.APP_BLUE_COLOR.cgColor : UIColor.red.cgColor
}
After long time i troubleshooted the problem and this is what worked for me...
I was doing following thing in cellForItemAtIndexPath: which is WRONG
cell.isSelected = false
collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .left)
I was doing this as didDeselect was getting called alternately without considering the selection of cell. I just uncommeted this code and it worked for me. Now didSelect is calling on first click and if i click on same cell again then only didDeselect is called as per expected flow.
Also make sure allowsMultipleSelection is true and also allowsSelection is true
I guess didselect method is called when you tap on second cell. Might be you forgot:
_collectionView.allowsMultipleSelection = YES
In my case i had a separate delegate class, and if i set collectionView.delegate = MyDelegate(), it didnt work, if i stored a var myDelegate = MyDelegate()in my viewcontroller and then set collectionView.delegate = myDelegate it somehow worked.
I am new to iOS development and am trying to create a PokeDex app. My main view controller has a collection view which returns two types of custom cells. My first cell called the chooser cell is immediately underneath the navigation bar or header bar whichever term, and I want my second custom cell(descriptioncell) to be immediately underneath the choosercell and take up the entire screen. I originally set the height of the description cell to be the height of the view - 50 to take the chooser cell into account but I saw that there was some space left over which shows the color of the collectionview. I then decided to make the descriptionview the height of the entire view.frame but there is still empty space for some reason. It is not a lot of space and by making the collectionview the same color as my descriptioncell everything would look fine, but why is this happening?
code in viewcontroller:
import UIKit
class PokeDexController: UICollectionViewController, UICollectionViewDelegateFlowLayout {
private let cellID = "cellID"
private let cellID2 = "cellID2"
override func viewDidLoad() {
super.viewDidLoad()
self.title = "PokeDex 386"
//collectionView?.backgroundColor = UIColor(red: 52/255.0, green: 55/255.0, blue: 64/255.0, alpha: 1.0)
collectionView?.backgroundColor = UIColor.white
collectionView?.register(chooserCell.self, forCellWithReuseIdentifier: cellID)
collectionView?.register(DescriptionCell.self, forCellWithReuseIdentifier: cellID2)
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
if (indexPath.row == 0)
{
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: cellID, for: indexPath) as! chooserCell
return cell
}
else
{
let cell2 = collectionView.dequeueReusableCell(withReuseIdentifier: cellID2, for: indexPath) as! DescriptionCell
return cell2
}
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 2
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if(indexPath.row == 0)
{
return CGSize(width: view.frame.width, height: 50)
}
else
{
return CGSize(width: view.frame.width, height: view.frame.height)
}
}
}
how the app currently looks, empty space between two cells
Thanks
I would double check view.frame to make sure it is what you expect it to be, just to rule out something simple. More times that I'd like to admit i've used references to another view who's frame was not what I thought it was going to be.
Another thing to check would be the content insets/spacing of the UICollectionView especially if you're using storyboards, I don't know if storyboards set a default inset/cell spacing or not. If you always seem to have some spacing around the edges of your cells its most likely some padding/spacing configured on the UICollectionView itself.
Also a likely culprit can be AutoLayout constraints, by default storyboards attach to layout margins not the layout edges. The margins are inset slightly from the true edge creating a gap.
Hopefully that helps some or at least gets you looking in the right places
Problem looks like this: http://i.imgur.com/5iaAiGQ.mp4
(red is a color of cell.contentView)
Here is the code: https://github.com/nezhyborets/UICollectionViewContentsAnimationProblem
Current status:
The content of UICollectionViewCell's contentView does not animate alongside contentView frame change. It gets the size immediately without animation.
Other issues faced when doing the task:
The contentView was not animating alongside cell's frame change either, until i did this in UICollectionViewCell subclass:
override func awakeFromNib() {
super.awakeFromNib()
//Because contentView won't animate along with cell
contentView.frame = bounds
contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
}
Other notes:
Here is the code involved in cell size animation
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.selectedIndex = indexPath.row
collectionView.performBatchUpdates({
collectionView.reloadData()
}, completion: nil)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let isSelected = self.selectedIndex == indexPath.row
let someSize : CGFloat = 90 //doesn't matter
let sizeK : CGFloat = isSelected ? 0.9 : 0.65
let size = CGSize(width: someSize * sizeK, height: someSize * sizeK)
return size
}
I get the same results when using collectionView.setCollectionViewLayout(newLayout, animated: true), and there is no animation at all when using collectionView.collectionViewLayout.invalidateLayout() instead of reloadData() inside batchUpdates.
UPDATE
When I print imageView.constraints inside UICollectionView's willDisplayCell method, it prints empty array.
func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
for view in cell.contentView.subviews {
print(view.constraints)
}
//Outputs
//View: <UIImageView: 0x7fe26460e810; frame = (0 0; 50 50); autoresize = RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x608000037280>>
//View constraints: []
}
This is a finicky problem, and you're very close to the solution. The issue is that the approach to animating layout changes varies depending on whether you're using auto layout or resizing masks or another approach, and you're currently using a mix in your ProblematicCollectionViewCell class. (The other available approaches would be better addressed in answer to a separate question, but note that Apple generally seems to avoid using auto layout for cells in their own apps.)
Here's what you need to do to animate your particular cells:
When cells are selected or deselected, tell the collection view layout object that cell sizes have changed, and to animate those changes to the extent it can do so. The simplest way to do that is using performBatchUpdates, which will cause new sizes to be fetched from sizeForItemAt, and will then apply the new layout attributes to the relevant cells within its own animation block:
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.selectedIndex = indexPath.row
collectionView.performBatchUpdates(nil)
}
Tell your cells to layout their subviews every time the collection view layout object changes their layout attributes (which will occur within the performBatchUpdates animation block):
// ProblematicCollectionViewCell.swift
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
layoutIfNeeded()
}
If you want greater control over your animations, you can nest the call to performBatchUpdates inside a call to one of the UIView.animate block-based animation methods. The default animation duration for collection view cells in iOS 10 is 0.25.
The solution is very easy. First, in ViewController.collectionView(_,didSelectItemAt:), write only this:
collectionView.performBatchUpdates({
self.selectedIndex = indexPath.row
}, completion: nil)
And then, in the class ProblematicCollectionViewCell add this func:
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
self.layoutIfNeeded()
}
Enjoy!
You can apply a transform to a cell, although it has some drawbacks, such as handling orientation changes.
For extra impact, I have added a color change and a spring effect in the mix, neither of which could be achieved using the reloading route:
func collectionView(_ collectionView: UICollectionView,
didSelectItemAt indexPath: IndexPath) {
UIView.animate(
withDuration: 0.4,
delay: 0,
usingSpringWithDamping: 0.4,
initialSpringVelocity: 0,
options: UIViewAnimationOptions.beginFromCurrentState,
animations: {
if( self.selectedIndexPath.row != NSNotFound) {
if let c0 =
collectionView.cellForItem(at: self.selectedIndexPath)
{
c0.contentView.layer.transform = CATransform3DIdentity
c0.contentView.backgroundColor = UIColor.lightGray
}
}
self.selectedIndexPath = indexPath
if let c1 = collectionView.cellForItem(at: indexPath)
{
c1.contentView.layer.transform =
CATransform3DMakeScale(1.25, 1.25, 1)
c1.contentView.backgroundColor = UIColor.red
}
},
completion: nil)
}