Top alignment of dynamic collection view cells should be same - ios

We are displaying a collection view with two cells in a row and displaying the label (dynamic with text) in each collectionviewcell.
We are able to achieve this by
override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes
The two cells in a row data are aligned as centered. can we make this to top-aligned?
Example:
Assume there are two cells with 2 lines of text in one cell and 4 lines of text in 2nd cell.
In this case, these two labels are centering aligned with each other. Can we make them top aligned?
Thanks in advance,
Ram

I have used AlignedCollectionViewFlowLayout for the quick solution.
https://github.com/mischa-hildebrand/AlignedCollectionViewFlowLayout

I have aprx same functionality so I am sharing what I have done in working condition. If your requirement is slightly different then you need to do more customization.
For code explanation, I suggest you to go through references links mentioned in last section of answer.
Create Custom FlowLayout Class
class CustomLayout: UICollectionViewFlowLayout {
private var computedContentSize: CGSize = .zero
private var cellAttributes = [IndexPath: UICollectionViewLayoutAttributes]()
var array = [String]()
override var collectionViewContentSize: CGSize {
return computedContentSize
}
override func prepare() {
computedContentSize = .zero
cellAttributes = [IndexPath: UICollectionViewLayoutAttributes]()
var preY = 10
var highY = 10
for section in 0 ..< collectionView!.numberOfSections {
var i = 0
for item in 0 ..< collectionView!.numberOfItems(inSection: section) {
let awidth = Int((self.collectionView?.bounds.width)!) / 3 - 15
let aheight = self.array[item].height(withConstrainedWidth: CGFloat(awidth), font: UIFont.systemFont(ofSize: 16)) + 20
var aX = 10
if item % 3 == 1 {
aX = aX + 10 + awidth
}
else if item % 3 == 2 {
aX = aX + 20 + 2 * awidth
}
else {
aX = 10
preY = highY
}
if highY < preY + Int(aheight) {
highY = preY + Int(aheight) + 5
}
let itemFrame = CGRect(x: aX, y: preY, width: awidth, height: Int(aheight))
let indexPath = IndexPath(item: item, section: section)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = itemFrame
cellAttributes[indexPath] = attributes
i += 1
}
}
computedContentSize = CGSize(width: (self.collectionView?.bounds.width)!,
height: CGFloat(collectionView!.numberOfItems(inSection: 0) * 70))
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var attributeList = [UICollectionViewLayoutAttributes]()
for (_, attributes) in cellAttributes {
if attributes.frame.intersects(rect) {
attributeList.append(attributes)
}
}
return attributeList
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cellAttributes[indexPath]
}
}
MainViewController
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
#IBOutlet weak var myColView: UICollectionView!
var array = ["Hello", "Look Behind", "Gotta get there", "Hey buddy", "Earth is rotating around the sun", "Sky is blue", "Kill yourself", "Humble docters", "Lets make party tonight Lets make party tonight", "Lets play PUB-G", "Where are you?", "Love you Iron Man."]
override func viewDidLoad() {
super.viewDidLoad()
let myLayout = CustomLayout()
myLayout.array = self.array
myColView.collectionViewLayout = myLayout
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return array.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "MyCell", for: indexPath) as! MyCell
cell.myLabel.text = array[indexPath.row]
cell.myLabel.layer.borderColor = UIColor.lightGray.cgColor
cell.myLabel.layer.borderWidth = 1
return cell
}
}
Used extensions
extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.height)
}
func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect, options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.width)
}
}
Output:
Reference:
https://www.raywenderlich.com/4829472-uicollectionview-custom-layout-tutorial-pinterest
https://www.netguru.com/codestories/practical-introduction-to-custom-uicollectionview-layouts
https://developer.apple.com/documentation/uikit/uicollectionview/customizing_collection_view_layouts

Related

the UICollectionView cell is centered why?

I had a problem with the collectionView.
In fact, I had this algorithm :
- the first cell's width is screen_width/2
- the second cell's width is : screen_width
- the third cell's width is screen_width/2
- the fourth cell's width is : screen_width.
and so on.
This is my code in the method
- (CGSize)collectionView:(UICollectionView *)collectionView
layout:(UICollectionViewLayout *)collectionViewLayout
sizeForItemAtIndexPath:(NSIndexPath *)indexPath:
userStory.typePhotoStory = ( (indexPath.row + 1) % 3) == 0 ? #"landscape" : ((indexPath.row + 1) % 3) == 1 ? #"portrait": #"landscape";
if ([userStory.typePhotoStory isEqualToString:#"portrait"]) {
cellSizeFinalWidth = cellSize / 2 ;
cellSizeFinalHeight = cellSizeFinalWidth + 20 ;
} else {
cellSizeFinalWidth = cellSize + padding ;
cellSizeFinalHeight = cellSize / 2 + 20 ;
}
}
But I see that, The first and the third cell are placed in the center of the screen , not started from the left.
Please help me on this issue?
You can directly use this code,
Create a new file and paste this below code for custom layout.
import UIKit
protocol NewLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, SizeOfCellAtIndexPath indexPath:IndexPath) -> CGSize
}
class NewFlowLayout: UICollectionViewFlowLayout {
weak var delegate : NewLayoutDelegate?
fileprivate var numberOfColumns = 1
fileprivate var cellPadding: CGFloat = 6
fileprivate var cache = [UICollectionViewLayoutAttributes]()
fileprivate var contentHeight: CGFloat = 0
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
super.prepare()
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
let xOffset : CGFloat = 0
let column = 0
var yOffset = [CGFloat](repeating: 0, count: numberOfColumns)
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let aSize = delegate?.collectionView(collectionView, SizeOfCellAtIndexPath: indexPath)
let height = cellPadding * 2 + (aSize?.height)!
let frame = CGRect(x: xOffset, y: yOffset[column], width: (aSize?.width)!, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
contentHeight = max(contentHeight, frame.maxY)
yOffset[column] = yOffset[column] + height
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
}
In your controller file
import UIKit
class NewCell: UICollectionViewCell {
}
class NewViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, NewLayoutDelegate {
#IBOutlet weak var myCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
myCollectionView.collectionViewLayout = NewFlowLayout()
if let layout = myCollectionView.collectionViewLayout as? NewFlowLayout {
layout.delegate = self
}
}
// Do size logic here
func collectionView(_ collectionView: UICollectionView, SizeOfCellAtIndexPath indexPath: IndexPath) -> CGSize {
let insets = collectionView.contentInset
if indexPath.item % 2 == 0 {
return CGSize(width: (collectionView.frame.size.width - (insets.left + insets.right))/2,
height: 60)
}
return CGSize(width: collectionView.frame.size.width - (insets.left + insets.right),
height: 60)
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 10
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "NewCell", for: indexPath) as! NewCell
return cell
}
}
Output

UICollectionView Cell Custom FlowLayout Breaks When New Post is Added on Top

Description:
I have been working on an app which displays images and caption in a CollectionView. I have made a custom Cell and CustomFlowLayout for the cell where the width of the cell, image and caption is equal to width of screen and height will change according to aspect ratio of the image height and height needed for the caption. The images which are displayed are stored on FirebaseStorage and I have also saved imagewidth and imageheight in firebaseDatabase. I Use SD_Web images to load and cache images. When the app loads for the first time the layout works perfectly fine as expected. The cells are arranged according to image height and caption height , with images being in aspect ratio.
The main problem occurs when a new post is done. I insert new post on the top of collectionview, and when the post is received the layout breaks, images becomes messed up, the caption goes on top image, lot of white space between image or caption. when I terminate the app from background and run it again , this time the layout is perfectly fine with new post present. I have to terminate app after everyone post to make the layout work.
What I feel the problem is, when I post the image. The new post is added on the top cell and the item on top cell pushed below without having the old height. how can I take this problem. I tried searching a lot but still of no use.
PS:
Using Autolayout for cell in IB.
FactsFeverLayout Class
import UIKit
protocol FactsFeverLayoutDelegate: class {
func collectionView(CollectionView: UICollectionView, heightForThePhotoAt indexPath: IndexPath, with width: CGFloat) -> CGFloat
func collectionView(CollectionView: UICollectionView, heightForCaptionAt indexPath: IndexPath, with width: CGFloat) -> CGFloat
}
class FactsFeverLayout: UICollectionViewLayout {
var cellPadding : CGFloat = 5.0
var delegate: FactsFeverLayoutDelegate?
private var contentHeight : CGFloat = 0.0
private var contentWidth : CGFloat {
let insets = collectionView!.contentInset
return (collectionView!.bounds.width - insets.left + insets.right)
}
private var attributeCache = [FactsFeverLayoutAttributes]()
override func prepare() {
if attributeCache.isEmpty {
let containerWidth = contentWidth
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
for item in 0 ..< collectionView!.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let width = containerWidth - cellPadding * 2
let photoHeight:CGFloat = (delegate?.collectionView(CollectionView: collectionView!, heightForThePhotoAt: indexPath, with: width))!
let captionHeight: CGFloat = (delegate?.collectionView(CollectionView: collectionView!, heightForCaptionAt: indexPath, with: width))!
let height: CGFloat = cellPadding + photoHeight + captionHeight + cellPadding
let frame = CGRect(x: xOffset, y: yOffset, width: containerWidth, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
// Create CEll Layout Atributes
let attributes = FactsFeverLayoutAttributes(forCellWith: indexPath)
attributes.photoHeight = photoHeight
attributes.frame = insetFrame
attributeCache.append(attributes)
// Update The Colunm any Y axis
contentHeight = max(contentHeight, frame.maxY)
yOffset = yOffset + height
}
}
}
override var collectionViewContentSize: CGSize{
return CGSize(width: contentWidth, height: contentHeight)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attributes in attributeCache {
if attributes.frame.intersects(rect){
layoutAttributes.append(attributes)
}
}
return layoutAttributes
}
}
// UICollectionView FlowLayout
// Abstract
class FactsFeverLayoutAttributes: UICollectionViewLayoutAttributes {
var photoHeight : CGFloat = 0.0
override func copy(with zone: NSZone? = nil) -> Any {
let copy = super.copy(with: zone) as! FactsFeverLayoutAttributes
copy.photoHeight = photoHeight
return copy
}
override func isEqual(_ object: Any?) -> Bool {
if let attributes = object as? FactsFeverLayoutAttributes {
if attributes.photoHeight == photoHeight {
super.isEqual(object)
}
}
return false
}
}
CollectionViewCell Class
class NewCellCollectionViewCell: UICollectionViewCell {
var facts: Facts!
var currentUser = Auth.auth().currentUser?.uid
// IBOutlets
#IBOutlet weak var imageView: UIImageView!
#IBOutlet weak var imageHeightConstraint: NSLayoutConstraint!
#IBOutlet weak var likeLable: UILabel!
#IBOutlet weak var likeButton: UIButton!
#IBOutlet weak var infoButton: UIButton!
#IBOutlet weak var buttonView: UIView!
#IBOutlet weak var captionTextView: UITextView!
override func awakeFromNib() {
super.awakeFromNib()
likeButton.setImage(UIImage(named: "noLike"), for: .normal)
likeButton.setImage(UIImage(named: "like"), for: .selected)
setupLayout()
}
func configureCell(fact: Facts){
facts = fact
imageView.sd_setImage(with: URL(string: fact.factsLink))
likeLable.text = String(fact.factsLikes.count)
captionTextView.text = fact.captionText
let factsRef = Database.database().reference().child("Facts").child(facts.factsId).child("likes")
factsRef.observeSingleEvent(of: .value) { (snapshot) in
if fact.factsLikes.contains(self.currentUser!){
self.likeButton.isSelected = true
} else {
self.likeButton.isSelected = false
}
}
}
override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) {
super.apply(layoutAttributes)
if let attributes = layoutAttributes as? FactsFeverLayoutAttributes {
imageHeightConstraint.constant = attributes.photoHeight
}
}
}
ViewController Class
class ViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
//MARK: Outlets
#IBOutlet weak var uploadButtonOutlet: UIBarButtonItem!
#IBOutlet weak var collectionView: UICollectionView!
//MARK:- Properties
var images: [UIImage] = []
var factsArray:[Facts] = [Facts]()
var likeUsers:[String] = []
let currentUser = Auth.auth().currentUser?.uid
private let refreshControl = UIRefreshControl()
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib.
if #available(iOS 10.0, *) {
collectionView.refreshControl = refreshControl
} else {
collectionView.addSubview(refreshControl)
}
refreshControl.addTarget(self, action: #selector(refreshView), for: .valueChanged)
refreshControl.tintColor = UIColor.white
if let layout = collectionView?.collectionViewLayout as? FactsFeverLayout {
layout.delegate = self
}
collectionView.backgroundColor = UIColor.black
observeFactsFromFirebase()
}
#objc func refreshView(){
observeFactsFromFirebase()
}
//MARK:- Upload Facts
#IBAction func uploadButtonPressed(_ sender: Any) {
self.selectPhoto()
(deleted the function of selectPhoto but it works, UIImagePicker is used)
}
private func uploadImageToFirebaseStorage(image: UIImage, completion: #escaping (_ imageUrl: String) -> ()){
let imageName = NSUUID().uuidString + ".jpg"
let ref = Storage.storage().reference().child("message_images").child(imageName)
if let uploadData = image.jpegData(compressionQuality: 0.2){
ref.putData(uploadData, metadata: nil, completion: { (metadata, error) in
if error != nil {
print(" Failed to upload Image", error)
}
ref.downloadURL(completion: { (url, err) in
if let err = err {
print("Unable to upload image into storage due to \(err)")
}
let messageImageURL = url?.absoluteString
completion(messageImageURL!)
})
})
}
}
func addToDatabase(imageUrl:String, caption: String, image: UIImage){
let Id = NSUUID().uuidString
likeUsers.append(currentUser!)
let timeStamp = NSNumber(value: Int(NSDate().timeIntervalSince1970))
let factsDB = Database.database().reference().child("Facts")
let factsDictionary = ["factsLink": imageUrl, "likes": likeUsers, "factsId": Id, "timeStamp": timeStamp, "captionText": caption, "imageWidth": image.size.width, "imageHeight": image.size.height] as [String : Any]
factsDB.child(Id).setValue(factsDictionary){
(error, reference) in
if error != nil {
print(error)
ProgressHUD.showError("Image Upload Failed")
self.uploadButtonOutlet.isEnabled = true
return
} else{
print("Message Saved In DB")
ProgressHUD.showSuccess("image Uploded Successfully")
self.uploadButtonOutlet.isEnabled = true
self.observeFactsFromFirebase()
}
}
}
var imageUrl: [String] = []
func observeFactsFromFirebase(){
let factsDB = Database.database().reference().child("Facts").queryOrdered(byChild: "timeStamp")
factsDB.observe(.value){ (snapshot) in
print("Observer Data snapshot \(snapshot.value)")
self.factsArray = []
self.imageUrl = []
self.likeUsers = []
if let snapshot = snapshot.children.allObjects as? [DataSnapshot] {
for snap in snapshot {
if let postDictionary = snap.value as? Dictionary<String, AnyObject> {
let id = snap.key
let facts = Facts(dictionary: postDictionary)
self.factsArray.insert(facts, at: 0)
self.imageUrl.insert(facts.factsLink, at: 0)
}
}
}
self.collectionView.reloadData()
self.refreshControl.endRefreshing()
}
collectionView.reloadData()
}
// Download Image From Database
//-> Here I download image from firebase and store it locally and append it to images array (Deleted the code to remove unwanted clutter)
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////
//MARK: Data Source
extension ViewController: UICollectionViewDataSource{
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return factsArray.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let facts = factsArray[indexPath.row]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "newCellTrial", for: indexPath) as? NewCellCollectionViewCell
cell?.configureCell(fact: facts)
cell?.infoButton.addTarget(self, action: #selector(reportButtonPressed), for: .touchUpInside)
return cell!
}
}
extension ViewController: UICollectionViewDelegate {
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
collectionView.deselectItem(at: indexPath, animated: true)
let photos = IDMPhoto.photos(withURLs: imageUrl)
let browser = IDMPhotoBrowser(photos: photos)
browser?.setInitialPageIndex(UInt(indexPath.row))
self.present(browser!, animated: true, completion: nil)
}
}
extension ViewController: FactsFeverLayoutDelegate {
func collectionView(CollectionView: UICollectionView, heightForThePhotoAt indexPath: IndexPath, with width: CGFloat) -> CGFloat {
let facts = factsArray[indexPath.item]
let imageSize = CGSize(width: CGFloat(facts.imageWidht), height: CGFloat(facts.imageHeight))
let boundingRect = CGRect(x: 0, y: 0, width: width, height: CGFloat(MAXFLOAT))
let rect = AVMakeRect(aspectRatio: imageSize, insideRect: boundingRect)
return rect.size.height
}
func collectionView(CollectionView: UICollectionView, heightForCaptionAt indexPath: IndexPath, with width: CGFloat) -> CGFloat {
let fact = factsArray[indexPath.item]
let topPadding = CGFloat(8)
let bottomPadding = CGFloat(8)
let captionFont = UIFont.systemFont(ofSize: 15)
let viewHeight = CGFloat(40) //-> There is view below caption which holds like button and info button its height is constant (40)
let captionHeight = self.height(for: fact.captionText, with: captionFont, width: width)
let height = topPadding + captionHeight + topPadding + viewHeight + bottomPadding + topPadding + 10
return height
}
func height(for text: String, with font: UIFont, width: CGFloat) -> CGFloat {
let nsstring = NSString(string: text)
let maxHeight = CGFloat(1000)
let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
let textAttributes = [NSAttributedString.Key.font: font]
let boundingRect = nsstring.boundingRect(with: CGSize(width: width, height: maxHeight), options: options, attributes: textAttributes, context: nil)
return ceil(boundingRect.height)
}
}
as you have used attributeCache, layoutAttribute of First Cell is Already Stored in cache.when you add images at first place and reload your collectionView , old Attribute for First cell will be retired from Cache and applying to your collectionView layout.
so you have to remove Element From Cache before reloading
override func prepare() {
attributeCache.RemoveAll()
if attributeCache.isEmpty {
let containerWidth = contentWidth
var xOffset : CGFloat = 0
var yOffset : CGFloat = 0
for item in 0 ..< collectionView!.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let width = containerWidth - cellPadding * 2
let photoHeight:CGFloat = (delegate?.collectionView(CollectionView: collectionView!, heightForThePhotoAt: indexPath, with: width))!
let captionHeight: CGFloat = (delegate?.collectionView(CollectionView: collectionView!, heightForCaptionAt: indexPath, with: width))!
let height: CGFloat = cellPadding + photoHeight + captionHeight + cellPadding
let frame = CGRect(x: xOffset, y: yOffset, width: containerWidth, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
// Create CEll Layout Atributes
let attributes = FactsFeverLayoutAttributes(forCellWith: indexPath)
attributes.photoHeight = photoHeight
attributes.frame = insetFrame
attributeCache.append(attributes)
// Update The Colunm any Y axis
contentHeight = max(contentHeight, frame.maxY)
yOffset = yOffset + height
}
}
}

CollectionView Flowlayout Customize

I am making a profile picture collectionview like tinder edit profile pictures. I want first cell bigger than others and 2, 3 cells besides first cell and others should like 3, 4, 5.
Any suggestion?
extension ViewController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
if indexPath.item == 0 {
return CGSize(width: 213.34, height: 213.34)
} else {
return CGSize(width: 101.66, height:101.66 )
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return 6
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "cell", for: indexPath)
let lbl = cell.viewWithTag(1) as! UILabel
lbl.text = String(format: "%d", indexPath.row + 1)
return cell
}
}
You need to implement an UICollectionViewLayout, I had called it FillingLayout, Note that you can adjust the number of columns and the size of your big cells with the delegate methods
Explanation
You need to add an Array to track your columns heigths and see what is the shortest column that is private var columsHeights : [CGFloat] = [] and you need also an array of (Int,Float) tuple to keep which spaces are available to be filled, I also added a method in the delegate to get the number of columns we want in the collection View and a method to know if a cell can be added or not in a position according their size.
Then if we want to add a cell we check if can be added if not, because the first column is filled we add the space corresponding to column2 in the avaiableSpaces array and when we add the next cell first we check if can be added in any available space if can be added we add and remove the available space.
here is the full code
import UIKit
protocol FillingLayoutDelegate: class {
func collectionView(_ collectionView:UICollectionView, sizeForViewAtIndexPath indexPath:IndexPath) -> Int
// Returns the amount of columns that have to display at that moment
func numberOfColumnsInCollectionView(collectionView:UICollectionView) ->Int
}
class FillingLayout: UICollectionViewLayout {
weak var delegate: FillingLayoutDelegate!
fileprivate var cellPadding: CGFloat = 10
fileprivate var cache = [UICollectionViewLayoutAttributes]()
fileprivate var contentHeight: CGFloat = 0
private var columsHeights : [CGFloat] = []
private var avaiableSpaces : [(Int,CGFloat)] = []
fileprivate var contentWidth: CGFloat {
guard let collectionView = collectionView else {
return 0
}
let insets = collectionView.contentInset
return collectionView.bounds.width - (insets.left + insets.right)
}
var columnsQuantity : Int{
get{
if(self.delegate != nil)
{
return (self.delegate?.numberOfColumnsInCollectionView(collectionView: self.collectionView!))!
}
return 0
}
}
//MARK: PRIVATE METHODS
private func shortestColumnIndex() -> Int{
var retVal : Int = 0
var shortestValue = MAXFLOAT
var i = 0
for columnHeight in columsHeights {
//debugPrint("Column Height: \(columnHeight) index: \(i)")
if(Float(columnHeight) < shortestValue)
{
shortestValue = Float(columnHeight)
retVal = i
}
i += 1
}
//debugPrint("shortest Column index: \(retVal)")
return retVal
}
//MARK: PRIVATE METHODS
private func largestColumnIndex() -> Int{
var retVal : Int = 0
var largestValue : Float = 0.0
var i = 0
for columnHeight in columsHeights {
//debugPrint("Column Height: \(columnHeight) index: \(i)")
if(Float(columnHeight) > largestValue)
{
largestValue = Float(columnHeight)
retVal = i
}
i += 1
}
//debugPrint("shortest Column index: \(retVal)")
return retVal
}
private func canUseBigColumnOnIndex(columnIndex:Int,size:Int) ->Bool
{
if(columnIndex < self.columnsQuantity - (size-1))
{
let firstColumnHeight = columsHeights[columnIndex]
for i in columnIndex..<columnIndex + size{
if(firstColumnHeight != columsHeights[i]) {
return false
}
}
return true
}
return false
}
override var collectionViewContentSize: CGSize {
return CGSize(width: contentWidth, height: contentHeight)
}
override func prepare() {
// Check if cache is empty
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
// Set all column heights to 0
self.columsHeights = []
for _ in 0..<self.columnsQuantity {
self.columsHeights.append(0)
}
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
let viewSize: Int = delegate.collectionView(collectionView, sizeForViewAtIndexPath: indexPath)
let blockWidth = (contentWidth/CGFloat(columnsQuantity))
let width = blockWidth * CGFloat(viewSize)
let height = width
var columIndex = self.shortestColumnIndex()
var xOffset = (contentWidth/CGFloat(columnsQuantity)) * CGFloat(columIndex)
var yOffset = self.columsHeights[columIndex]
if(viewSize > 1){//Big Cell
if(!self.canUseBigColumnOnIndex(columnIndex: columIndex,size: viewSize)){
// Set column height
for i in columIndex..<columIndex + viewSize{
if(i < columnsQuantity){
self.avaiableSpaces.append((i,yOffset))
self.columsHeights[i] += blockWidth
}
}
// Set column height
yOffset = columsHeights[largestColumnIndex()]
xOffset = 0
columIndex = 0
}
for i in columIndex..<columIndex + viewSize{
if(i < columnsQuantity){
//current height
let currValue = self.columsHeights[i]
//new column height with the update
let newValue = yOffset + height
//space that will remaing in blank, this must be 0 if its ok
let remainder = (newValue - currValue) - CGFloat(viewSize) * blockWidth
if(remainder > 0) {
debugPrint("Its bigger remainder is \(remainder)")
//number of spaces to fill
let spacesTofillInColumn = Int(remainder/blockWidth)
//we need to add those spaces as avaiableSpaces
for j in 0..<spacesTofillInColumn {
self.avaiableSpaces.append((i,currValue + (CGFloat(j)*blockWidth)))
}
}
self.columsHeights[i] = yOffset + height
}
}
}else{
//if there is not avaiable space
if(self.avaiableSpaces.count == 0)
{
// Set column height
self.columsHeights[columIndex] += height
}else{//if there is some avaiable space
yOffset = self.avaiableSpaces.first!.1
xOffset = CGFloat(self.avaiableSpaces.first!.0) * width
self.avaiableSpaces.remove(at: 0)
}
}
print(width)
let frame = CGRect(x: xOffset, y: yOffset, width: width, height: height)
let insetFrame = frame.insetBy(dx: cellPadding, dy: cellPadding)
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attributes.frame = insetFrame
cache.append(attributes)
contentHeight = max(contentHeight, frame.maxY)
}
}
func getNextCellSize(currentCell: Int, collectionView: UICollectionView) -> Int {
var nextViewSize = 0
if currentCell < (collectionView.numberOfItems(inSection: 0) - 1) {
nextViewSize = delegate.collectionView(collectionView, sizeForViewAtIndexPath: IndexPath(item: currentCell + 1, section: 0))
}
return nextViewSize
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var visibleLayoutAttributes = [UICollectionViewLayoutAttributes]()
// Loop through the cache and look for items in the rect
for attributes in cache {
if attributes.frame.intersects(rect) {
visibleLayoutAttributes.append(attributes)
}
}
return visibleLayoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
}
UPDATED
You need to setup your viewController as FillingLayoutDelegate
override func viewDidLoad() {
super.viewDidLoad()
collectionView.delegate = self
collectionView.dataSource = self
// Do any additional setup after loading the view.
if let layout = self.collectionView.collectionViewLayout as? FillingLayout
{
layout.delegate = self
}
}
FillingLayoutDelegate implementation in your ViewController
extension ViewController: FillingLayoutDelegate{
func collectionView(_ collectionView:UICollectionView,sizeForViewAtIndexPath indexPath:IndexPath) ->Int{
if(indexPath.row == 0 || indexPath.row == 4)
{
return 2
}
if(indexPath.row == 5)
{
return 3
}
return 1
}
func numberOfColumnsInCollectionView(collectionView:UICollectionView) ->Int{
return 3
}
}
ScreenShot working
You can use UICollectionViewLayout to handle this. Code is given below:
UICollectionViewLayout class to define layout:
class CustomCircularCollectionViewLayout: UICollectionViewLayout {
var itemSize = CGSize(width: 200, height: 150)
var attributesList = [UICollectionViewLayoutAttributes]()
override func prepare() {
super.prepare()
let itemNo = collectionView?.numberOfItems(inSection: 0) ?? 0
let length = (collectionView!.frame.width - 40)/3
itemSize = CGSize(width: length, height: length)
attributesList = (0..<itemNo).map { (i) -> UICollectionViewLayoutAttributes in
let attributes = UICollectionViewLayoutAttributes(forCellWith: IndexPath(item: i, section: 0))
attributes.size = self.itemSize
var x = CGFloat(i%3)*(itemSize.width+10) + 10
var y = CGFloat(i/3)*(itemSize.width+10) + 10
if i > 2 {
y += (itemSize.width+10)
attributes.frame = CGRect(x: x, y: y, width: itemSize.width, height: itemSize.height)
} else if i == 0 {
attributes.frame = CGRect(x: x, y: y, width: itemSize.width*2+10, height: itemSize.height*2+10)
} else {
x = itemSize.width*2 + 30
if i == 2 {
y += itemSize.height + 10
}
attributes.frame = CGRect(x: x, y: y, width: itemSize.width, height: itemSize.height)
}
return attributes
}
}
override var collectionViewContentSize : CGSize {
return CGSize(width: collectionView!.bounds.width, height: (itemSize.height + 10)*CGFloat(ceil(Double(collectionView!.numberOfItems(inSection: 0))/3))+(itemSize.height + 20))
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
return attributesList
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
if indexPath.row < attributesList.count
{
return attributesList[indexPath.row]
}
return nil
}
override func shouldInvalidateLayout(forBoundsChange newBounds: CGRect) -> Bool {
return true
}
Now set this class as your collection view layout:
self.collectionView!.collectionViewLayout = circularLayoutObject
This will display your grid as given below:
Let me know if it works for you.

Custom UICollectionViewFlowLayout not working

I am trying to do a custom Flow Layout similar to the Apple News app. The flowLayout.delegate = self is in the viewDidLoad() method while my networking code is in in an async method in the viewDidAppear().
The problem is that the methods for the custom flow Layout get called before I can retrieve all the data from the server, therefore the app crashes.
Any ideas on how I could make it work? Here's my ViewController implementation:
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, AppleNewsFlowLayoutDelegate {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var flowLayout: AppleNewsFlowLayout!
var newsArray = [News]()
var getFromDb = GetFromDb()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if getFromDb.news.isEmpty {
loadStore()
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
flowLayout.delegate = self
}
func loadStore() {
let urlString = "https://url"
self.getFromDb.getBreaksFromDb(url: urlString) { (breaksDataCell) in
if !breaksDataCell.isEmpty {
DispatchQueue.main.async(execute: {
self.collectionView.reloadData()
})
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return getFromDb.news.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
// Filling the cells with the correct info...
return cell
}
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType {
return getFromDb.news[indexPath.row].cellType
}
}
Here below the struct for News:
struct News {
let image: String
let provider: String
let title: String
let cellType: NewsCellType
init(image: String, provider: String, title: String, cellType: NewsCellType) {
self.image = image
self.provider = provider
self.title = title
self.cellType = cellType
}}
The Flow Layout class:
protocol AppleNewsFlowLayoutDelegate: class {
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType
}
class AppleNewsFlowLayout: UICollectionViewFlowLayout {
var maxY: CGFloat = 0.0
var isVSetOnce = false
weak var delegate: AppleNewsFlowLayoutDelegate?
var attributesArray: [UICollectionViewLayoutAttributes]?
private var numberOfItems:Int{
return (collectionView?.numberOfItems(inSection: 0))!
}
override func prepare() {
for item in 0 ..< numberOfItems{
super.prepare()
minimumLineSpacing = 10
minimumInteritemSpacing = 16
sectionInset = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16)
}
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
guard let newsType: NewsCellType = delegate?.AppleNewsFlowLayout(self, cellTypeForItemAt: indexPath) else {
fatalError("AppleNewsFlowLayoutDelegate method is required.")
}
let screenWidth = UIScreen.main.bounds.width
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
var x = sectionInset.left
maxY = maxY + minimumLineSpacing
switch newsType {
case .big:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: width * 1.2)
maxY += width * 1.2
case .horizontal:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: 150)
maxY += 150
case .vertical:
let width = (screenWidth - minimumInteritemSpacing - sectionInset.left - sectionInset.right) / 2
x = isVSetOnce ? x + width + minimumInteritemSpacing : x
maxY = isVSetOnce ? maxY-10 : maxY
attributes.frame = CGRect(x: x, y: maxY, width: width, height: screenWidth * 0.8)
if isVSetOnce {
maxY += screenWidth * 0.8
}
isVSetOnce = !isVSetOnce
}
return attributes
}
override var collectionViewContentSize: CGSize {
return CGSize(width: UIScreen.main.bounds.width, height: maxY)
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
if attributesArray == nil {
attributesArray = [UICollectionViewLayoutAttributes]()
print(collectionView!.numberOfItems(inSection: 0) - 1)
for i in 0 ... collectionView!.numberOfItems(inSection: 0) - 1
{
let attributes = self.layoutAttributesForItem(at: IndexPath(item: i, section: 0))
attributesArray!.append(attributes!)
}
}
return attributesArray
}
}
Two things which you need to do when we deal with custom collection view flow layouts.
changes in prepare() method in custom flow layout. you will start preparing the layout only if the number of items more than 0.
private var numberOfItems:Int{
return (collectionView?.numberOfItems(inSection: 0))!
}
override func prepare() {
for item in 0 ..< numberOfItems{ }
}
use numberOfItems property whenever you wanted to do something with collectionView items in the customFlowLayout to avoid crashes.
Here below the functioning clean code if you are looking to implement a Custom Collection View Flow Layout similar to the Apple News app with networking:
class ViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, AppleNewsFlowLayoutDelegate {
#IBOutlet weak var collectionView: UICollectionView!
#IBOutlet weak var flowLayout: AppleNewsFlowLayout!
var getFromDb = GetFromDb()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if getFromDb.news.isEmpty {
loadStore()
}
}
override func viewDidLoad() {
super.viewDidLoad()
collectionView.dataSource = self
collectionView.delegate = self
flowLayout.delegate = self
}
func loadStore() {
let urlString = "https://yourURL"
self.getFromDb.getBreaksFromDb(url: urlString) { (breaksDataCell) in
if !breaksDataCell.isEmpty {
DispatchQueue.main.async(execute: {
self.collectionView.reloadData()
print("RELOAD")
})
}
}
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return getFromDb.news.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let news = getFromDb.news[indexPath.row]
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: news.cellType.rawValue, for: indexPath) as! NewCell
cell.image.image = UIImage(named: "news")!
cell.provider.text = news.provider
cell.title.text = news.title
return cell
}
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType {
print(getFromDb.news[indexPath.row].cellType)
return getFromDb.news[indexPath.row].cellType
}
}
This is the Flow Layout class:
protocol AppleNewsFlowLayoutDelegate: class {
func AppleNewsFlowLayout(_ AppleNewsFlowLayout: AppleNewsFlowLayout, cellTypeForItemAt indexPath: IndexPath) -> NewsCellType
}
class AppleNewsFlowLayout: UICollectionViewFlowLayout {
var maxY: CGFloat = 0.0
var isVSetOnce = false
weak var delegate: AppleNewsFlowLayoutDelegate?
var attributesArray: [UICollectionViewLayoutAttributes]?
private var cache = [UICollectionViewLayoutAttributes]()
private var numberOfItems:Int{
return (collectionView?.numberOfItems(inSection: 0))!
}
override func prepare() {
super.prepare()
minimumLineSpacing = 10
minimumInteritemSpacing = 16
sectionInset = UIEdgeInsets(top: 10, left: 16, bottom: 10, right: 16)
guard cache.isEmpty == true, let collectionView = collectionView else {
return
}
let screenWidth = UIScreen.main.bounds.width
var x = sectionInset.left
maxY = maxY + minimumLineSpacing
for item in 0 ..< collectionView.numberOfItems(inSection: 0) {
let indexPath = IndexPath(item: item, section: 0)
guard let newsType: NewsCellType = delegate?.AppleNewsFlowLayout(self, cellTypeForItemAt: indexPath) else {
fatalError("AppleNewsFlowLayoutDelegate method is required.")
}
let attributes = UICollectionViewLayoutAttributes(forCellWith: indexPath)
switch newsType {
case .big:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: width * 1.2)
maxY += width * 1.2
case .horizontal:
let width = screenWidth - sectionInset.left - sectionInset.right
attributes.frame = CGRect(x: x, y: maxY, width: width, height: 150)
maxY += 150
case .vertical:
let width = (screenWidth - minimumInteritemSpacing - sectionInset.left - sectionInset.right) / 2
x = isVSetOnce ? x + width + minimumInteritemSpacing : x
maxY = isVSetOnce ? maxY-10 : maxY
attributes.frame = CGRect(x: x, y: maxY, width: width, height: screenWidth * 0.8)
if isVSetOnce {
maxY += screenWidth * 0.8
}
isVSetOnce = !isVSetOnce
}
cache.append(attributes)
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
var layoutAttributes = [UICollectionViewLayoutAttributes]()
for attribute in cache{
if attribute.frame.intersects(rect){
layoutAttributes.append(attribute)
}
}
return layoutAttributes
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
return cache[indexPath.item]
}
override var collectionViewContentSize: CGSize {
return CGSize(width: UIScreen.main.bounds.width, height: maxY)
}
}

iOS: in Swift3, in my collectionView, when I scroll downstair, the collectionView comes automatically at the top. How to stop it?

I would like my collectionView to stop in the cell. The problem is that when I scroll downstair, if I release it, it comes automatically at the first position...
I tried with collection.alwaysBounceVertical = true without success...
Here is my code:
override func viewDidLoad() {
super.viewDidLoad()
let layout = OrganiserLayout()
let frameCollection = CGRect(x: self.view.frame.origin.x, y: self.view.frame.origin.y, width: self.view.frame.width, height: self.view.frame.height + 1000)
let collection = UICollectionView(frame: frameCollection, collectionViewLayout: layout)
collection.delegate = self
collection.dataSource = self
collection.backgroundColor = UIColor.white
collection.register(OrganiserCollectionViewCell.self, forCellWithReuseIdentifier: "cell")
self.view.addSubview(collection)
collection.alwaysBounceVertical = true
collection.alwaysBounceHorizontal = true
collection.bounces = true
collection.isPagingEnabled = false
collection.isScrollEnabled = true
}
func numberOfSections(in collectionView: UICollectionView) -> Int {
return 15
}
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
switch section {
case 0:
return 40
}
}
class OrganiserLayout:UICollectionViewLayout {
let cellWidth:CGFloat = 100
var attrDict = Dictionary<IndexPath,UICollectionViewLayoutAttributes>()
var contentSize = CGSize.zero
override var collectionViewContentSize : CGSize {
return self.contentSize
}
override func prepare() {
// Generate the attributes for each cell based on the size of the collection view and our chosen cell width
if let cv = collectionView {
let collectionViewHeight = cv.frame.height
let numberOfSections = cv.numberOfSections
self.contentSize = cv.frame.size
self.contentSize.width = cellWidth*CGFloat(numberOfSections)
for section in 0...numberOfSections-1 {
let numberOfItemsInSection = cv.numberOfItems(inSection: section)
let itemHeight = collectionViewHeight/CGFloat(numberOfItemsInSection)
let itemXPos = cellWidth*CGFloat(section)
for item in 0...numberOfItemsInSection-1 {
let indexPath = IndexPath(item: item, section: section)
let itemYPos = itemHeight*CGFloat(item)
let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attr.frame = CGRect(x: itemXPos, y: itemYPos, width: cellWidth, height: itemHeight)
attrDict[indexPath] = attr
}
}
}
}
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
// Here we return the layout attributes for cells in the current rectangle
var attributesInRect = [UICollectionViewLayoutAttributes]()
for cellAttributes in attrDict.values {
if rect.intersects(cellAttributes.frame) {
attributesInRect.append(cellAttributes)
}
}
return attributesInRect
}
override func layoutAttributesForItem(at indexPath: IndexPath) -> UICollectionViewLayoutAttributes? {
// Here we return one attribute object for the specified indexPath
return attrDict[indexPath]!
}
}
class OrganiserCollectionViewCell:UICollectionViewCell {
var label:UILabel!
var seperator:UIView!
override init(frame: CGRect) {
super.init(frame: frame)
label = UILabel()
label.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(label)
seperator = UIView()
seperator.backgroundColor = UIColor.black
seperator.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(seperator)
let views:[String:UIView] = [
"label":label,
"sep":seperator
]
let cons = [
"V:|-20-[label]",
"V:[sep(1)]|",
"H:|[label]|",
"H:|[sep]|"
]
for con in cons {
self.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: con, options: [], metrics: nil, views: views))
}
}
I need that because I need the user stops on the cell, and click on it. With that problem, he cannot click on the cells that are downstairs.
Thanks in advance.
Set some height for contentSize like below :
self.contentSize.height = CGFloat(2750)
In your code :
class OrganiserLayout:UICollectionViewLayout {
override func prepare() {
// Generate the attributes for each cell based on the size of the collection view and our chosen cell width
if let cv = collectionView {
let collectionViewHeight = cv.frame.height
let numberOfSections = cv.numberOfSections
self.contentSize = cv.frame.size
self.contentSize.width = cellWidth*CGFloat(numberOfSections)
self.contentSize.height = CGFloat(2750) // I added this line
for section in 0...numberOfSections-1 {
let numberOfItemsInSection = cv.numberOfItems(inSection: section)
let itemHeight = collectionViewHeight/CGFloat(numberOfItemsInSection)
let itemXPos = cellWidth*CGFloat(section)
for item in 0...numberOfItemsInSection-1 {
let indexPath = IndexPath(item: item, section: section)
let itemYPos = itemHeight*CGFloat(item)
let attr = UICollectionViewLayoutAttributes(forCellWith: indexPath)
attr.frame = CGRect(x: itemXPos, y: itemYPos, width: cellWidth, height: itemHeight)
attrDict[indexPath] = attr
}
}
}
}
}

Resources