Preserve Image Aspect Ratio in UICollectionViewCell - swift

Problem: Reloaded cached UIImageView in a UICollectionViewCell (during scrolling or switching between UICollectionViewController) are not preserving the correct contentMode.
I tried using .sizeToFit() on the UIImageView and resetting .contentMode = UIViewContentMode.ScaleAspectFit on every load, but that wasn't helping.
override func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> MyCollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier("myCell", forIndexPath: indexPath) as! MyCollectionViewCell
if ( indexPath.item < self.the_array?.count ){
if ( images_cache[self.the_array![indexPath.item].src!] != nil ){
// calling this here doesn't work
cell.myImage.contentMode = UIViewContentMode.ScaleAspectFit
// calling this here doesn't work either
cell.myImage.sizeToFit()
cell.myImage.image = images_cache[self.the_array![indexPath.item].src!]! as UIImage
}
else{
if let url = NSURL(string: "https:" + (self.the_array![indexPath.item].src)!), data = NSData(contentsOfURL: url) {
let the_image = UIImage( data: data )
cell.myImage.image = the_image
images_cache[self.the_array![indexPath.item].src!] = the_image!
}
}
}
return cell
}

Solution: Set the .frame of the UIImage based on the size of the cell!
cell.myImage.frame = CGRect(x: 0, y: 0, width: cell.frame.width, height: cell.frame.height)
I put that line where I had .sizeToFit() and / or .contentMode and now it preserves the images correctly.

Related

tableView renders slowly with UIImages

I have a custom cell that populates the tableView with users post on the app. The user can post a text message or a text message with a uiimage.
The initial problem i had was in setting the height of the cell when i contained an images. I ended up having image sin the tableview cells with ridiculous height and the images were not loading correctly.
so i added the code below to the tableView CellforRowAt function, in order to alter the frame of the images so that the UIImages loaded in a uniform size. Although this is working perfectly now, when i scroll on the tableview it is super glitchy and slow. I assume its because of the frame being altered and it takes a while to process this new frame. Is it possible to make this more efficient and smoother?
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let cell = tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell
cell.set(post: posts[indexPath.row])
cell.delegate = self
cell.commentDelegate = self
cell.likeDelegate = self
cell.selectionStyle = .none
let imageIndex = posts[indexPath.row].mediaURL
let url = NSURL(string: imageIndex)! as URL
if let imageData: NSData = NSData(contentsOf: url) {
let image = UIImage(data: imageData as Data)
let newWidth = cell.MediaPhoto.frame.width
let scale = newWidth/image!.size.width
let newHeight = image!.size.height * scale
UIGraphicsBeginImageContext(CGSize(width: newWidth, height: newHeight))
image!.draw(in: CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
cell.MediaPhoto.image = newImage
cell.MediaPhoto.contentMode = .scaleToFill
cell.MediaPhoto.clipsToBounds = true
}
}
return cell
}
Try moving the work outside of the main thread.
class PostTableViewCell: UITableViewCell {
var image: UIImage? {
didSet {
setNeedsDisplay()
}
}
func set(post: Post) {
DispatchQueue.global().async { [weak self] in
let strongSelf = self!
if let url = NSURL(string: strongSelf.mediaUrl) as URL?,
let imageData: NSData = NSData(contentsOf: url) {
strongSelf.image = UIImage(data: imageData as Data)
}
}
}
override func draw(_ rect: CGRect) {
guard let image = image else { super.draw(rect) }
let newWidth = MediaPhoto.frame.width
let scale = newWidth/image.size.width
let newHeight = image.size.height * scale
UIGraphicsBeginImageContext(CGSize(width: newWidth, height: newHeight))
image!.draw(in: CGRect(x: 0, y: 0, width: newWidth, height: newHeight))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
MediaPhoto.image = newImage
MediaPhoto.contentMode = .scaleToFill
MediaPhoto.clipsToBounds = true
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "postCell", for: indexPath) as! PostTableViewCell
if indexPath.section == 0 {
cell.set(post: posts[indexPath.row])
cell.delegate = self
cell.commentDelegate = self
cell.likeDelegate = self
cell.selectionStyle = .none
}
return cell
}
I ended up using this code to fix the image size to be 400 x 400 and it worked great. It doesn't render slowly or glitch anymore.
func set(post:Posts) {
self.post = post
// get PROFILE image
let url = post.url
profileImageView.sd_setImage(with: url, completed: nil)
// get MEDIA image
let mediaString = post.urlM.absoluteString
if mediaString == "none" {
MediaPhoto.image = nil
} else {
let mediaUrl = post.urlM
MediaPhoto.sd_setImage(with: mediaUrl, completed: nil)
//RESIZE MEDIA PHOTO
let itemSize:CGSize = CGSize(width: 400, height: 400)
UIGraphicsBeginImageContextWithOptions(itemSize, false, UIScreen.main.scale)
let imageRect : CGRect = CGRect(x: 0, y: 0, width: itemSize.width, height: itemSize.height)
MediaPhoto!.image?.draw(in: imageRect)
MediaPhoto!.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
}

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.
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:
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
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.
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
}
}
}
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.

How to show image from both url and uiimage in the same time in collection view in swift?

I need to show images from both url and uiimage in a collectionview.
The scenerio is images are coming from the api and user can add other image in the collection view.
Here is the code that I am using right now ?
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return savedPhotosArray.count + 1
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = checkOutCollectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath)as! CheckOutCollectionViewCell
if indexPath.row == 0{
let img = UIImageView(frame: CGRect(x: 0, y: 0, width: cell.frame.size.width, height: cell.frame.size.height))
img.layer.cornerRadius = 10
cell.addSubview(img)
img.image = #imageLiteral(resourceName: "plus")
img.backgroundColor = blackThemeColor
}else{
let img2 = UIImageView(frame: CGRect(x: 0, y: 0, width: cell.frame.size.width, height: cell.frame.size.height))
img2.layer.cornerRadius = 10
img2.clipsToBounds = true
cell.addSubview(img2)
if let image = savedPhotosArray[indexPath.row - 1] as? String
{
let imageStr = basePhotoUrl + image
print (imageStr)
let imageUrl = URL(string: imageStr)
img2.sd_setImage(with: imageUrl , placeholderImage: #imageLiteral(resourceName: "car-placeholder"))
}
}
Here index = 0 is used to add images from imagePicker.
You need to create a struct like this
struct Item {
var isLocal:Bool
var imageNameUrl:String
}
//
var savedPhotosArray = [Item(isLocal:true, imageNameUrl:"local.png"),Item(isLocal:false, imageNameUrl:"remoteExtension")]
//
Also don't add the images inside cellForRowAt as cells are reusable and this glitches the UI plus memory leaks , make the imageView inside the cell xib or if programmatically and assign only in cellForRowAt

Swift - cellForItemAtIndexPath unwrapping an Optional value when resizing image

i need resize dynamically an imagView when scroll a collection view in swift...
i use this function for get event of scrolling:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// get a reference to our storyboard cell
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! MyCollectionViewCell
let indexPathItem = self.items[indexPath.item]
if let myImage = cell.myImage{
myImage.image = imageWithImage(UIImage(named: indexPathItem)!, scaledToSize: CGSize(width: sizeImage, height: sizeImage))
}
return cell
}
and this function for resize image:
func imageWithImage(image:UIImage,scaledToSize newSize:CGSize)->UIImage{
UIGraphicsBeginImageContext( newSize )
image.drawInRect(CGRect(x: 0,y: 0,width: newSize.width,height: newSize.height))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage.imageWithRenderingMode(.AlwaysTemplate)
}
debugged stops when run this line:
myImage.image = imageWithImage(UIImage(named: indexPathItem)!, scaledToSize: CGSize(width: sizeImage, height: sizeImage))
with error
"fatal error: unexpectedly found nil while unwrapping an Optional
value"
working if i replace that with this:
cell.myImage.image = UIImage(named : self.items[indexPath.item])
But obviously it does not reduce the image
Can help me pls???
SOLUTION:
i need resize object "cell.myImage" and not "cell.myImage.image" so solved with:
cell.myImage.frame = CGRect(x:0.0,y:0.0,width:sizeImage,height:sizeImage)
and set mode of imageView into cell like aspect fit.
You could use safer code like below to avoid crashes while unwrapping an Optional value.
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
// get a reference to our storyboard cell
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! YourCell
cell.myImage?.image = imageWithImage(UIImage(named: self.items[indexPath.item]), scaledToSize: CGSize(width: sizeImage, height: sizeImage))
return cell
}
func imageWithImage(image:UIImage?,scaledToSize newSize:CGSize)->UIImage?{
guard let drawImage = image else {
return nil
}
UIGraphicsBeginImageContext( newSize )
drawImage.drawInRect(CGRect(x: 0,y: 0,width: newSize.width,height: newSize.height))
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage?.imageWithRenderingMode(.AlwaysTemplate)
}
your cellForItemAtIndexPath code is incomplete below line
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as!
so I have added YourCell for example.

UICollectionView image flickering

I have a UICollectionView that shows cells at fullscreen (i.e. a gallery).
Sometimes, when swiping for the new image, an image other than the right one is displayed for less than a second, than the image is updated.
Other times, the wrong image is shown, but if you swipe left and than right (i.e. the image disappear and reappear) than the right image comes up.
I don't understand the cause of this behavior.
Images are downloaded asynchronously when the collection view needs them.
Here is the code:
let blank = UIImage(named: "blank.png")
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! UICollectionViewCell
let image = images[indexPath.row]
if let imageView = cell.viewWithTag(5) as? UIImageView {
imageView.image = blank
if let cachedImage = cache.objectForKey(image.url) as? UIImage {
imageView.image = cachedImage
} else {
UIImage.asyncDownloadImageWithUrl(image.url, completionBlock: { (succeded, dimage) -> Void in
if succeded {
self.cache.setObject(dimage!, forKey: image.url)
imageView.image = dimage
}
})
}
}
return cell
}
where UIImage.asyncDownloadImageWithUrl is:
extension UIImage {
static func asyncDownloadImageWithUrl(url: NSURL, completionBlock: (succeeded: Bool, image: UIImage?) -> Void) {
let request = NSMutableURLRequest(URL: url)
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: { (response, data, error) in
if error == nil {
if let image = UIImage(data: data) {
completionBlock(succeeded: true, image: image)
}
} else {
completionBlock(succeeded: false, image: nil)
}
})
}
}
and for the first image shown:
func collectionView(collectionView: UICollectionView, willDisplayCell cell: UICollectionViewCell, forItemAtIndexPath indexPath: NSIndexPath) {
if let index = self.index {
let newIndexPath = NSIndexPath(forItem: index, inSection: 0)
self.collectionView.scrollToItemAtIndexPath(newIndexPath, atScrollPosition: UICollectionViewScrollPosition.Left, animated: false)
self.index = nil
}
}
This is what i think it's happening:
Cell A appears
Request for image A is made to load on Cell A
Cell A disappears from screen
Cell A reappears (is reused)
Request for image B is made to load on Cell A
Request for image A is complete
Image A loads on to the Cell A
Request for image B is complete
Image B loads on to the Cell A
This is happening because you are not keeping track of which image url should load on the completion block.
Try this:
One way to do that is to store the url of the image that should be there in your UICollectionViewCell. To do that, create a subclass:
class CustomCollectionViewCell:UICollectionViewCell {
var urlString = ""
}
Then:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCellWithReuseIdentifier(reuseIdentifier, forIndexPath: indexPath) as! CustomCollectionViewCell
let image = images[indexPath.row]
if let imageView = cell.viewWithTag(5) as? UIImageView {
imageView.image = blank
cell.urlString = image.url.absoluteString
if let cachedImage = cache.objectForKey(image.url) as? UIImage {
imageView.image = cachedImage
} else {
UIImage.asyncDownloadImageWithUrl(image.url, completionBlock: { (succeded, dimage) -> Void in
if succeded {
self.cache.setObject(dimage!, forKey: image.url)
//
// This can happen after the cell has dissapeared and reused!
// check that the image.url matches what is supposed to be in the cell at that time !!!
//
if cell.urlString == image.url.absoluteString {
imageView.image = dimage
}
}
})
}
}
return cell
}
For a more accurate reply, post the project (or a barebones version of it). It's a lot of work to reproduce your setup and test.