How to add an UICollectionViewLayout programmatically? - swift

I'm trying to setup a UICollectionViewLayout programmatically. I'm using a UICollectionView also without using storyboards, as well as settings its constraints.
I've followed Ray Wenderlich tutorial on the subject with some changes to adapt the code to Swift 3 (as AZCoder2 did https://github.com/AZCoder2/Pinterest).
Since all these examples uses storyboards, I've also introduced some changes to create the UICollectionView and its UICollectionViewLayout:
collectionViewLayout = PinterestLayout()
collectionViewLayout.delegate = self
collectionView = UICollectionView.init(frame: .zero, collectionViewLayout: collectionViewLayout)
The result: I can't see anything. If I change the UICollectionViewLayout with the one that Apple provides (UICollectionViewFlowLayout), at least I can see the cells with their content. If I implement some changes and use the storyboard, everything works great but it's not the way I want to accomplish this. The whole view is made programmatically and the collection view is a part of it.
What am I missing? Is it something to do with the way I instantiate the UICollectionViewLayout? Do I have to register something (for example, as I need to register the reusable cell)?

How about you just create a variable that creates your flow layout for you like this
var flowLayout: UICollectionViewFlowLayout {
let _flowLayout = UICollectionViewFlowLayout()
// edit properties here
_flowLayout.itemSize = CGSize(width: 98, height: 134)
_flowLayout.sectionInset = UIEdgeInsetsMake(0, 5, 0, 5)
_flowLayout.scrollDirection = UICollectionViewScrollDirection.horizontal
_flowLayout.minimumInteritemSpacing = 0.0
// edit properties here
return _flowLayout
}
And then you can set it by calling the variable.
self.collectionView.collectionViewLayout = flowLayout // after initializing it another way
// or
UICollectionView(frame: .zero, collectionViewLayout: flowLayout)

I think this will work.
override init(collectionViewLayout layout: UICollectionViewLayout) {
super.init(collectionViewLayout: layout)
collectionView?.collectionViewLayout = YourCollectionViewLayout()
}

It's possible to do what I was trying to do. Basically, follow the tutorials I suggested in my own question and setup the collection view and its view layout as follow:
collectionViewLayout = PinterestLayout()
collectionViewLayout.delegate = self
collectionView = DynamicCollectionView.init(frame: .zero, collectionViewLayout: collectionViewLayout)
Note that I'm using DynamicCollectionView (instead of UICollectionView). This class is not provided by Apple: I've made my own using the code provided in this post.
Remember that this approach is when you're creating a view programmatically, using constraints. (May be it has another cases of use)

I resolved this issue taking these steps:
1 - Changing UICollectionViewController to a UIViewController with a collectionView inside.
2 - On ViewDidLoad I set the delegates and add the view, as usual. Moreover, I instantiate the ViewLayout and use it to instantiate the CollectionView
var collectionView: UICollectionView?
override func viewDidLoad() {
super.viewDidLoad()
navigationController!.isToolbarHidden = true
let layout = MosaicViewLayout()
layout.delegate = self
collectionView = UICollectionView(frame: .zero, collectionViewLayout: layout)
guard let collectionView = collectionView else {
return
}
view.addSubview(collectionView)
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(PhotoCell.self, forCellWithReuseIdentifier: "CharacterCell")
setupCollectionConstraints()
}
3 - On the ViewLayout the protocol stays like this:
protocol MosaicViewLayoutDelegate:class {
func collectionView(_ collectionView: UICollectionView,
heightForItemAtIndexPath indexPath: IndexPath) -> CGFloat
}
Don't forget to add the instance inside the class
weak var delegate: MosaicViewLayoutDelegate?
4 - Add the delegate extension to you ViewController
extension HomeViewImpl: MosaicViewLayoutDelegate {
func collectionView(_ collectionView: UICollectionView, heightForItemAtIndexPath indexPath: IndexPath) -> CGFloat {
let random = arc4random_uniform(4) + 1
return CGFloat(random * 100)
}
}
enter code here

Related

Vertical and Horizontal Scrolling When Embedding a Table View in a Collection View that has a Collection View Header and Horizontal Flow Layout

I am trying to implement an application that allows a user to swipe horizontally between collection view cells while also being able to scroll vertically to see the entire content of a particular cell.
I want all the collection view cells to have a table view embedded within them.
The problem I am running into as of now is that my horizontal scrolling works as I have set the collection view flow layout to horizontal. I know that collection view flow layouts can only support one direction. Because of this, I tried to implement the following solution.
As of now within my project, I have a view controller with a scroll view inside. Embedded within the scroll view, is a collection view. This collection view has its own header implemented via dequeueReusableSupplementaryView.
I know that constraints can often be an issue preventing vertical scrolling to take place so here is a picture of my constraints:
Additionally, here is some code that I have used to implement this system:
Determining the scroll view content size
override func viewDidLayoutSubviews() {
super.viewWillLayoutSubviews()
scrollView.contentSize.height = collectionView.frame.size.height
scrollView.contentSize.width = self.view.frame.size.width
}
Setting up the collection view
func setupCollectionView() {
collectionView.dataSource = self
collectionView.delegate = self
collectionView.register(UINib(nibName: "TableViewHolderCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: "tableViewHolderCollectionViewCell")
collectionView.register(UINib(nibName: "CustomCollectionViewHeaderView", bundle: nil), forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "CustomCollectionViewHeaderView")
collectionView.delaysContentTouches = true
collectionView.contentInsetAdjustmentBehavior = .never
collectionView.bounces = false
collectionView.isPagingEnabled = true
let layout = UICollectionViewFlowLayout()
layout.scrollDirection = .horizontal
layout.minimumLineSpacing = 0
layout.sectionHeadersPinToVisibleBounds = true
layout.sectionInset = UIEdgeInsets(top: 0, left: -self.view.frame.size.width, bottom: 0, right: 0)
collectionView.setCollectionViewLayout(layout, animated: false)
}
Setting up the collection view data
extension TasksAndScheduleViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 8
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return CGSize(width: self.view.frame.size.width, height: self.view.frame.size.height)
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "tableViewHolderCollectionViewCell", for: indexPath) as! TableViewHolderCollectionViewCell
cell.backgroundColor = colorArray[indexPath.row]
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
return CGSize(width: collectionView.frame.width, height: 343)
}
func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
//setting up header view
return headerView
}
}
After trying to debug, I realized that the collection view vertical scroll might have been overriding the vertical scroll of the scroll view I had added to my view controller. In order to solve this, I created a custom class (as seen below) which my collection view implemented. To my knowledge, this was successful at disabling the vertical scroll for the collection view but it was not successful in enabling the other scroll view's vertical scroll.
class CollectionViewVerticalScroll: UICollectionView {
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
let direction = panGestureRecognizer.direction(in: self)
if direction.contains(.Down) || direction.contains(.Up) {
return false
}
return true
}
}
My desired goal is to have the ability to swipe horizontally between these collection view cells that have table views while also being able to vertically scroll the collection view cell and the contents of the table view embedded inside of it. Ideally, the vertical scroll should allow me to scroll the table view cells while also moving the entire view upwards. The closest example to what I am trying to implement that I could find online is twitter's search page. The only difference is that my application has a collection header view and no navigation bar. I have attached a picture below:
I would appreciate any help. Please do let me know if you have any questions or if something doesn't make sense to you.

Last UICollectionViewCell appearing different to the others

I'm creating a UICollectionView on my main menu and all the cells look fine bar the last one, where the image is not centered relative to the cell- it appears to be anchored to the top left corner of the cell (not sure about that though).
here is the image that contains the problem.
I didn't really know where to start with this, as all the cells use the same code and constraints.
I checked that it wasn't an image issue by repeating a previously used image (see above screenshot)
My only idea is that the final cell is in a row on its own whereas the other rows have two cells per row.
extension MainMenuViewController: UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return mainMenuOptions.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MainMenuCell", for: indexPath) as! MainMenuCollectionViewCell
let imageView = cell.mainMenuImage!
let label = cell.mainMenuLabel!
imageView.image = mainMenuImages[indexPath.item]
imageView.frame.size.height = cell.frame.size.width - 5
imageView.frame.size.width = cell.frame.size.width - 10
cell.mainMenuLabel.text = mainMenuOptions[indexPath.item]
let labelText = cell.mainMenuLabel!
label.frame.size.height = Utils.heightForView(text: labelText.text!, font: labelText.font!, width: cell.frame.width)
label.frame.size.width = cell.frame.size.width
label.center.x = cell.frame.size.width / 2
label.center.y = cell.mainMenuImage.frame.size.height + (cell.frame.size.height - cell.mainMenuImage.frame.size.height) / 2
cell.layer.borderColor = UIColor.darkGray.cgColor
cell.layer.borderWidth = CGFloat(0.5)
cell.layer.cornerRadius = CGFloat(10)
return cell
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MainMenuCell", for: indexPath) as! MainMenuCollectionViewCell
let padding: CGFloat = 40
let cellWidth = (collectionView.frame.size.width - padding) / 2
let labelText = mainMenuOptions[indexPath.item]
let cellHeight = cellWidth + Utils.heightForView(text: labelText, font: cell.mainMenuLabel.font!, width: cell.frame.width) + 70
return CGSize(width: cellWidth, height: cellHeight)
}
}
Utils.heightForView() is just a function that calculates the size required for a label to fit all the text. If you need to see it I'll happily add it.
Thanks very much in advance! I hope this is the right amount of code needed but if not let me know and I'll add more.
EDIT: Cell class
class MainMenuCollectionViewCell: UICollectionViewCell {
#IBOutlet weak var mainMenuLabel: UILabel!
#IBOutlet weak var mainMenuImage: UIImageView!
}
When you dequeue a cell in cellForItem it doesn't necessarily have its final size at that point, yet you're depending on that being correct for your code to work.
You should be using autolayout constraints or a stack view inside the cell to give you the correct layout without needing to do any work at dequeue time.
You seem to be trying to give variable heights for your cells as well, which is going to look pretty messy in a flow layout, although it doesn't seem to be making any difference to the cells in the screenshot which I'd expect to have different heights given their titles. Dequeuing a cell in sizeForItem could have some unexpected side effects too, since that's going to mess with the reuse pool.

Image does not fit properly in collectionCell

In Swift 3. My app has a table view and a detail view with is a view controller. Here I added a collection view. An array of images must be presented. The images adapt themselves beautifully if in portrait I check the content mode in scaler to fill. In ladscape it works in aspectFill. Vice-versa, the layout does not fit. I did the following
in cellForItem At:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = myCollection.dequeueReusableCell(withReuseIdentifier: "cellCollection", for: indexPath) as! CollectionViewCell
cell.cellImage.image = UIImage(named: array[indexPath.item])
if UIDevice.current.orientation.isLandscape{
cell.cellImage.contentMode = .scaleAspectFill
}
else if UIDevice.current.orientation.isPortrait{
cell.cellImage.contentMode = .scaleToFill
}
return cell
}
It does not work. May be I have some inconsistencies from portrait to landscape.
I would appreciate your help. If you want to take a look at the project in git:
[https://github.com/ricardovaldes/tareaFinalHoteles.git1. Thanks in advance.
Eureka. Finally I did solve this problem. With sizeForItem at, the app could function without problems. I was lost or mislead looking for a way to change the image content mode, but this is not the right way. The correct way is resize once you rotate: for this you need the func willTransition toNewCollection: UITraitCollection. Inside you create a variable layout that is of type UICollectionViewFlowLayout. With the help of a conditional, is possible to define how you want the with and heigt after rotation.This post is clear and concise for those curious: UICollectionView Cell Resizing on Device Rotation.
Here the implementation I did in my project: `
override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
if UIDevice.current.orientation.isLandscape,
let layout = myCollection.collectionViewLayout as? UICollectionViewFlowLayout{
let with = myCollection.frame.size.width
layout.itemSize = CGSize(width: with, height: 280)
layout.invalidateLayout()
}
else if UIDevice.current.orientation.isLandscape,
let layout = myCollection.collectionViewLayout as? UICollectionViewFlowLayout{
let with = myCollection.frame.size.width
layout.itemSize = CGSize(width: with/2, height: 280)

Tint of the image does not want to change

I am trying to set a new color of the image while the cell of the UICollectionView was selected or deselected. Whenever i do not set a tint color of the image it is working, but i do not wanna have default blue color of it. So What I am doing is :
In func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell function, I am inicialazing my images with code :
let borderWidth = itemBorder.frame.width
let borderHeight = itemBorder.frame.height
myImage = UIImageView(frame: CGRect(x: 0, y: 0, width: borderWidth - 1/4 * borderWidth, height: borderHeight - 1/5 * borderHeight))
myImage.image = UIImage(named: myCollection[indexPath.row])?.withRenderingMode(.alwaysTemplate)
myImage.tintColor = UIColor.groupTableViewBackground
myImage.center = CGPoint(x: tmpCell.bounds.width/2, y: tmpCell.bounds.height/2)
myImage.contentMode = .scaleAspectFit
tmpCell.contentView.addSubview(myImage)
and so on in the didselected and deselected function :
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
let currentCell: UICollectionViewCell?
switch collectionView {
case myCollectionView:
print("clicked")
currentCell = myCollectionView.cellForItem(at: indexPath)!
currentCell?.tintColor = UIColor.white
case colorsCollectionView:
break
default:
break
}
}
func collectionView(_ collectionView: UICollectionView, didDeselectItemAt indexPath: IndexPath) {
let currentCell: UICollectionViewCell?
switch collectionView {
case vehiclesCollectionView:
print("deselected")
currentCell = myCollectionView.cellForItem(at: indexPath)!
currentCell?.tintColor = UIColor.groupTableViewBackground
case colorsCollectionView:
break
default:
break
}
}
Could someone tell me whats wrong ?
Thanks in advance!
Solution
Solution
Whenever i wanted to update the tint of the color i was pointing on the cell not on the image so basiclly only need to add this into selected or deselected method :
currentCell = myCollectionView.cellForItem(at: indexPath)!
let image = currentCell?.contentView.subviews[1] as! UIImageView
image.tintColor = UIColor.white
I see that you are setting the currentCell?.tintColor but I suppose you will probably have to set the currentCell?.vehicleImageView.image.tintColor
Also I see your code as a bit confusing since you have vehicleImage (which should probably be named vehicleImageView) and myImage which is a UIImage which is being added as a subView to the contentView? I thought it was only possible to add subclasses of UIView as subviews.
I suggest you create an outlet called myImageView in your custom UICollectionViewCell to which you can set the image.tintColor and change cell.myImageView.tintColor in your didSelect and didDeselect
If you do not add them from the storyboard, you can still create a subclass of UICollectionViewCell that has a property called vehicleImageView. You can set the frame and image of this as required in your cellForRow. Now you will have a property which you can refer to in your didSelect and didDeselect as cell.vehicleImageView.image.tintColor.
If you do not want to create a subclass that has a property, you will basically have to loop through all your subviews and find the image view and set the image tintColor there. Setting the tintColor of the UICollectionViewCell WILL NOT solve the problem. You will have to set it to the imageView.image
Hope that helps!

UICollectionView rounded imageView

I have a UIImageView inside a UICollectionView Cell.
I wanted there to be 2 cells per column in the uicollectionview so I used this code....
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
let padding: CGFloat = 25
let collectionCellSize = collectionView.frame.size.width - padding
return CGSize(width: collectionCellSize/2, height: collectionCellSize/2)
}
For the image view I wanted it to be round, and this code usually works elsewhere..
self.accountImageView.layer.cornerRadius = frame.size.width/2
self.accountImageView.clipsToBounds = true
I have tried putting that in the cellForItemAt, with no luck
Now inside the CollectionView Cell Class I added it like this
override func layoutSubviews() {
super.layoutSubviews()
self.accountImageView.layer.cornerRadius = frame.size.width/2
self.accountImageView.clipsToBounds = true
}
The image looks like a deflated football.
Is the padding code messing up the rounded image view code?
You need to add self.accountImageView.layoutIfNeeded().
And make sure height and width of your imageview is equal
override func layoutSubviews()
{
super.layoutSubviews()
self.accountImageView.layoutIfNeeded() // add this
self.accountImageView.layer.cornerRadius = frame.size.width/2
self.accountImageView.clipsToBounds = true
}
If you are using constraint then try this -
override func layoutSubviews() {
super.layoutSubviews()
self.accountImageView.layoutIfNeeded() // Add this line
self.accountImageView.layer.cornerRadius = frame.size.width/2
self.accountImageView.clipsToBounds = true
}