Swift UITableViewCell Temporary Image While Downloading Image from API
Xcode: 14.3.1
iOS: 16 or above
GitHub Repo: https://github.com/costa-rica/UITableViewCell05.git
This article shows the dynamism of UITableViewCell.
Even with the help of ChatGPT and studying Swift for 4 months I have had problems getting the UITableView and UITableViewCell to do what I want.
Gif 1 UITableViewCell05 application
What does this application do
The user accesses the TableView. There are no images. So the application places a temporary image that looks like it's loading using Swifts native spinner object. The application calls Flickr API to get images. Once a response is received remove the spinner / temporary images. Then resize the downloaded images. Place them in the cells and resize the cell.
Also, make sure that the UITableView cell heights are dynamic. I had a lot of grief with this up until about three weeks ago when I realized I did not need to size the cells myself. If you are using heightForCellAt, there is a good chance you don’t need to and you can just let Swift do the work for you.
I’m going to touch on the key items that pertain to this dynamism that I am talking about which I had so much trouble leveraging.
Cell heights
This is all you need to do
Code Snippet 1 Cell height for dynamic height size
tbl.rowHeight = UITableView.automaticDimension
tbl.estimatedRowHeight = 100
To anyone struggling like I did this is really all there is if you are building an application like mine. I know other coders rarely make such strong statements because each script is different but I gave up on this bit of code and moved to a much more challenging less Swifty approach and I would just advise anyone thinking this doesn’t work to review your code and see if there is another problem. To this day I don’t know what I was doing wrong but after I developed more intuition I stripped away all my sizing lines of code and let these lines of code do the work and voila it worked.
View Controller
My general approach is to use a UIViewController then add the table in. This requires adding UITableViewDelegate and UITableViewDataSource as extensions. You could just add them to the bottom of the class, but I am of the habit add as extensions in the same file.
Register the custom UITableViewCell
It seems to me there are a couple ways to describe this: subclass UITableViewCell or custom UITableViewCell. ChatGPT seems to understand both of them and they seem to be technically right. That said I’m sure ChatGPT will make sense of other ways of trying to describe this but the key here is that in the application I did not use the native UITableViewCell for my UITableView.
tbl.register(PostCell.self, forCellReuseIdentifier: "PostCell")
tbl.rowHeight = UITableView.automaticDimension
tbl.estimatedRowHeight = 100
I think the snippet of code above in the ViewDidLoad of your UIViewController that hosts the table is all you need. I have the github repo for this app that should work once you download and run. You can see for yourself what else you might be missing but if you’re anything like me and those pieces of code weren’t working immediately you probably jumped to use the UITableViewDelegate method heightForRowAt. I am not sure what that is really used for, but as far as I can tell just letting the << tbl.rowHeight = UITableView.automaticDimension >> do the work for you is all that is necessary.
I probably only spent 20 or 30 minutes trying it and then bailed to use the heightForRowAt, which lead to months of extra work. I am not kidding. I was sizing and resizing everything any time I made a change. I’m sure I learned something but I also know it's not necessary.
I digress.
Add and remove Objects
When working with UITAbleViewCell and dequing cells you’ll need to add and override the prepareForReuse() method in your cell. There you’ll add code that looks like I have in mine.
override func prepareForReuse() {
super.prepareForReuse()
stckVwPost.removeFromSuperview()
lblUsername.removeFromSuperview()
if post.imageURL != nil {
dictImgVw?[post.imageName!]?.removeFromSuperview()
}
}
Why I am brining this up is because when your UITableViewCell has a temporary UIImageView while the image you want is downloading you’ll need to remove the temporary UIImageView, in this case the spinner from the view.
In this application I have created a dictionary ‘var dictImgVw:[String:UIImageView]?’ for this because I will develop this application to have varying number of images. But for this particular version you can just replace this with a ‘var imgVw:UIImageView?’ and then remove it when the downloaded image arrives.
Code Snippet 4 Add image method
func addImage(){
justShowSpinners(imgName: post.imageName!)
DispatchQueue.main.asyncAfter(deadline: .now() + 3.0){
self.flickrStore.fetchImageFromWeb(imgURL: self.post.imageURL!) { resultResponse in
switch resultResponse{
case let .success(uiimage):
self.dictImgVw![self.post.imageName!]?.removeFromSuperview()
self.imgVwSpinnerHeightConstraint?.isActive = false
let resizedUIImage = resizeImageToFitScreenWidth(uiimage)
self.dictImgVw![self.post.imageName!] = UIImageView(image: resizedUIImage)
self.stckVwPost.addArrangedSubview(self.dictImgVw![self.post.imageName!]!)
guard let tbl = self.superview as? UITableView else { return }
tbl.beginUpdates()
tbl.endUpdates()
case let .failure(error):
print("Error adding image: \(error)")
}
}
}
}
Resize after finishing updating the cell
This is pretty easy to do as you’ll notice in Code Snippet 1 there is a guard statement followed by << beginUPdates()>> and <<endUpdates>> these will resize all the cells in the table. I have tried to use a the reloadRows which reloads the data but I’m not sure it would do anything for us as far as sizing – or at least I wasn’t able to use it for this purpose. So beginUpdates and endupdates after each new image is fine.
Avoiding Conflicts
If you’ll notice in my code, I made a variable for the constraint for the height of the Spinner. The reason for this is to turn it off once the new image was added. However, that still produced conflicting constraints warnings in my terminal. So after some advice from ChatGPT, they suggested I lower the priority and viola, that did it. No more conflicting constraints. So if you run this application in your computer you should see a clean terminal.
Code Snippet 5 Spinner constraint
var imgVwSpinnerHeightConstraint: NSLayoutConstraint? // Declare constraint as UITableViewCell (PostCell) property
func createImgVwSpinner(imageAccessId:String) -> UIImageView {
/* Other Code here */
// Set constraints
spinner.centerXAnchor.constraint(equalTo: imgVwSpinner.centerXAnchor).isActive=true
spinner.centerYAnchor.constraint(equalTo: imgVwSpinner.centerYAnchor).isActive=true
imgVwSpinnerHeightConstraint = imgVwSpinner.heightAnchor.constraint(equalToConstant: 450)
imgVwSpinnerHeightConstraint?.priority = UILayoutPriority(rawValue: 999) // Just below the default high priority
imgVwSpinnerHeightConstraint?.isActive = true
return imgVwSpinner
}
This allows the cell to adjust the height of the spinner UIImageView so as not to cause any constraints issues. Since the size of the spinner isn’t critical, we can make this any size really. But I wanted it to be bigger than most pictures downloaded we can see the size of the cell change.