Roggie
Roggie

Reputation: 1217

My UICollectionView does not scroll smoothly using Swift

I have a CollectionView which dequeues a cell depending on the message type (eg; text, image).

The problem I am having is that when I scroll up/down the scroll is really choppy and thus not a very good user experience. This only happens the first time the cells are loaded, after that the scrolling is smooth.

Any ideas how I can fix this?, could this be an issue with the time its taking to fetch data before the cell is displayed?

I am not too familiar with running tasks on background threads etc. and not sure what changes I can make to prefect the data pre/fetching etc.. please help!

The Gif shows scroll up when the view loads, it shows the cells/view being choppy as I attempt to scroll up.

enter image description here

This is my func loadConversation() which loads the messages array

func loadConversation(){

        DataService.run.observeUsersMessagesFor(forUserId: chatPartnerId!) { (chatLog) in
            self.messages = chatLog
            DispatchQueue.main.async {
                self.collectionView.reloadData()

                if self.messages.count > 0 {
                    let indexPath = IndexPath(item: self.messages.count - 1, section: 0)

                    self.collectionView.scrollToItem(at: indexPath, at: .bottom , animated: false)

                }
            }
        }//observeUsersMessagesFor

    }//end func

This is my cellForItemAt which dequeues cells

func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {

        let message = messages[indexPath.item]

        let uid = Auth.auth().currentUser?.uid


        if message.fromId == uid {

            if message.imageUrl != nil {
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ConversationCellImage", for: indexPath) as! ConversationCellImage
                cell.configureCell(message: message)
                return cell

            } else {
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ConversationCellSender", for: indexPath) as! ConversationCellSender
                cell.configureCell(message: message)
                return cell

            }//end if message.imageUrl != nil


        } else {

            if message.imageUrl != nil {
                let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ConversationCellImageSender", for: indexPath) as! ConversationCellImageSender
                cell.configureCell(message: message)
                return cell

            } else {

            let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ConversationCell", for: indexPath) as! ConversationCell
            cell.configureCell(message: message)
            return cell

            }

        }//end if uid 

    }//end func

This is my ConversationCell class which configures a custom cell for dequeueing by cellForItemAt (note: in addition there another ConversationCellImage custom cell class which configures an image message):

class ConversationCell: UICollectionViewCell {

    @IBOutlet weak var chatPartnerProfileImg: CircleImage!
    @IBOutlet weak var messageLbl: UILabel!

    override func awakeFromNib() {
        super.awakeFromNib()


        chatPartnerProfileImg.isHidden = false

    }//end func

    func configureCell(message: Message){

        messageLbl.text = message.message

        let partnerId = message.chatPartnerId()


        DataService.run.getUserInfo(forUserId: partnerId!) { (user) in
            let url = URL(string: user.profilePictureURL)
            self.chatPartnerProfileImg.sd_setImage(with: url, placeholderImage:  #imageLiteral(resourceName: "placeholder"), options: [.continueInBackground, .progressiveDownload], completed: nil)

        }//end getUserInfo


    }//end func


    override func layoutSubviews() {
        super.layoutSubviews()

        self.layer.cornerRadius = 10.0
        self.layer.shadowRadius = 5.0
        self.layer.shadowOpacity = 0.3
        self.layer.shadowOffset = CGSize(width: 5.0, height: 10.0)
        self.clipsToBounds = false

    }//end func

    override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {

//toggles auto-layout
        setNeedsLayout()
        layoutIfNeeded()

        //Tries to fit contentView to the target size in layoutAttributes
        let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)

        //Update layoutAttributes with height that was just calculated
        var frame = layoutAttributes.frame
        frame.size.height = ceil(size.height) + 18
        layoutAttributes.frame = frame
        return layoutAttributes
    }

}//end class

Time Profile results:

enter image description here

Edit: Flowlayout code

if let flowLayout = self.collectionView.collectionViewLayout as? UICollectionViewFlowLayout,
    let collectionView = collectionView {
    let w = collectionView.frame.width - 40
    flowLayout.estimatedItemSize = CGSize(width: w, height: 200)
}// end if-let

Edit: preferredLayoutAttributesFitting function in my custom cell class

override func preferredLayoutAttributesFitting(_ layoutAttributes: UICollectionViewLayoutAttributes) -> UICollectionViewLayoutAttributes {
    //toggles auto-layout
    setNeedsLayout()
    layoutIfNeeded()

    //Tries to fit contentView to the target size in layoutAttributes
    let size = contentView.systemLayoutSizeFitting(layoutAttributes.size)

    //Update layoutAttributes with height that was just calculated
    var frame = layoutAttributes.frame
    frame.size.height = ceil(size.height) + 18
    layoutAttributes.frame = frame
    return layoutAttributes
}

SOLUTION:

extension ConversationVC: UICollectionViewDelegateFlowLayout{

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

        var height: CGFloat = 80

        let message = messages[indexPath.item]

        if let text = message.message {

            height = estimateFrameForText(text).height + 20

        } else if let imageWidth = message.imageWidth?.floatValue, let imageHeight = message.imageHeight?.floatValue{

            height = CGFloat(imageHeight / imageWidth * 200)

        }

        let width = collectionView.frame.width - 40

        return CGSize(width: width, height: height)
    }

    fileprivate func estimateFrameForText(_ text: String) -> CGRect {
        let size = CGSize(width: 200, height: 1000)
        let options = NSStringDrawingOptions.usesFontLeading.union(.usesLineFragmentOrigin)
        return NSString(string: text).boundingRect(with: size, options: options, attributes: [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 16)], context: nil)

    }

}//end extension

Upvotes: 3

Views: 6981

Answers (3)

Fogmeister
Fogmeister

Reputation: 77621

I think the choppiness you are seeing is because the cells are given a size and then override the size they were given which causes the layout to shift around. What you need to do is do the calculation during the creation of the layout first time through.

I have a function that I have used like this...

func height(forWidth width: CGFloat) -> CGFloat {
    // do the height calculation here
}

This is then used by the layout to determine the correct size first time without having to change it.

You could have this as a static method on the cell or on the data... or something.

What it needs to do is create a cell (not dequeue, just create a single cell) then populate the data in it. Then do the resizing stuff. Then use that height in the layout when doing the first layout pass.

Yours is choppy because the collection lays out the cells with a height of 20 (for example) and then calculates where everything needs to be based on that... then you go... "actually, this should be 38" and the collection has to move everything around now that you've given it a different height. This then happens for every cell and so causes the choppiness.

I might be able to help a bit more if I could see your layout code.

EDIT

Instead of using that preferredAttributes method you should implement the delegate method func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize.

Do something like this...

This method goes into the view controller.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
    let message = messages[indexPath.item]

    let height: CGFloat

    if let url = message.imageURL {
        height = // whatever height you want for the images
    } else {
        height = // whatever height you want for the text
    }

    return CGSize(width: collectionView.frame.width - 40, height: height)
}

You may have to add to this but it will give you an idea.

Once you've done this... remove all your code from the cell to do with changing the frames and attributes etc.

Also... put your shadow code into the awakeFromNib method.

Upvotes: 0

Krimi
Krimi

Reputation: 332

Always make sure that the data like image or GIF shouldn't download on the main thread.

This is the reason why your scrolling is not smooth. Download the data in separate thread in background use either GCD or NSOperation queue. Then show the downloaded image always on main thread.

use AlamofireImage pods, it will automatically handle the downloaded task in background.

import AlamofireImage

extension UIImageView {

func downloadImage(imageURL: String?, placeholderImage: UIImage? = nil) {
    if let imageurl = imageURL {
        self.af_setImage(withURL: NSURL(string: imageurl)! as URL, placeholderImage: placeholderImage) { (imageResult) in
            if let img = imageResult.result.value {
                self.image = img.resizeImageWith(newSize: self.frame.size)
                self.contentMode = .scaleAspectFill
            }
        }
    } else {
        self.image = placeholderImage
    }
}

}

Upvotes: 0

Bhavin Kansagara
Bhavin Kansagara

Reputation: 2916

First thing first, let's try finding the exact location where this problem is arising.

Try 1:

comment this line

//self.chatPartnerProfileImg.sd_setImage(with: url, placeholderImage:  #imageLiteral(resourceName: "placeholder"), options: [.continueInBackground, .progressiveDownload], completed: nil)

And run your app to see the results.

Try 2:

Put that line in async block to see the results.

DispatchQueue.main.async {
     self.chatPartnerProfileImg.sd_setImage(with: url, placeholderImage:  #imageLiteral(resourceName: "placeholder"), options: [.continueInBackground, .progressiveDownload], completed: nil)
}

Try 3: Comment the code for setting corner radius

/*self.layer.cornerRadius = 10.0
        self.layer.shadowRadius = 5.0
        self.layer.shadowOpacity = 0.3
        self.layer.shadowOffset = CGSize(width: 5.0, height: 10.0)
        self.clipsToBounds = false*/

Share your results for Try 1, 2 and 3 and then we can get the better idea about where the problem lies.

Hope this way we can get the reason behind flickering.

Upvotes: 1

Related Questions