Search code examples
iosswiftautolayoutuicollectionviewflowlayout

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)
        }


    }

Solution

  • 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
    
    
            }
    
    
        }
        }