In our app we often use UITableViews to display different custom UITableViewCells in a scrollable list by using a list of display items with a type as the data source. In the TableView's cellforRowAt function we then decide which custom UITableViewCell to use depending on the display item's type.
This approach is now causing a problem in a UITableViewCell that loads and displays an image from the internet as the view doesn't know the image's size yet.
This is the custom view I'm trying to build: see custom UITableViewCell
However, I tried to start with just the image inside of my custom view. As the TableView row doesn't know how big the image is going to be, no image is displayed when the view appears. Only after scrolling down and up to the point where the image should be, the cell is getting resized as I want it to be. I set top, leading and trailing constraints of the image to 0 so the image fills the cell completely.
What is best practice for loading images and displaying them in such cells with the cell being resized depending on the actual image size?
in viewDidLoad I set
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 80
cellForRowAtIndexPath
let cell: CustomTableViewCell = tableView.dequeueReusableCell(withIdentifier: "CustomTableViewCell", for: indexPath) as! CustomTableViewCell
cell.imageView.contentMode = .scaleAspectFit
tableView.beginUpdates()
cell.imageView.kf.setImage(with: URL(string: item.getImageUrl()), placeholder: nil, options: nil, progressBlock: nil) { (image, error, cacheType, url) in
tableView.endUpdates()
}
cell.isUserInteractionEnalbed = false
cell.separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: .greatestFiniteMagnitude)
cell.accessoryType = .none
return cell
When you are configuring your tableview or when your view loads:
write these:
yourTableView.estimatedRowHeight = 70.0 // this can be any height you want to set when table loads first time.
yourTableView.rowHeight = UITableViewAutomaticDimension
or you can set these settings from storyboard/xib if you are configuring it from there. see here settings for uitableview.
One solution can be reloading that particular row, if visible, after image downloading finishes.
tableView.reloadRows(at: [indexPathForRow], with: .none)
As you are saying that image is getting shown perfectly after scrolling down and up gain, I believe this solution should work.
Finally found the problem. The data I'm displaying is provided by a presenter and not there initially. That's why I'm calling tableView.reloadData() after I have it. The problem was that I didn't call that method explicitly in the main thread.
DispatchQueue.main.async {
tableView.reloadData()
}
Related
I am making a table view app which retrieves the data from the Firebase. when making the user interface in the storyboard, I am using dummy image and label to visualize my app.
but when i run the app which consists of dynamic table view, those dummy images and label also shows up before immediately replaced by the actual data that i download from the Firebase storage.
can I set those images and labels to not show up when i run the app but still available in the storyboard?
thanks in advance :)
If they're just dummies, you can get rid of them when your view loads, before it appears onscreen:
override func viewDidLoad() -> Void{
super.viewDidLoad()
dummy.removeFromSuperview()
}
Whenever you want to hide/show an UIView:
myView.isHidden = true // hide
myView.isHidden = false // show
I assume what you need is to hide the views in viewWillAppear and then show them when necessary.
In your custom cell class, define something to hide the unwanted views:
func hideDummyViews() {
// do some stuff to hide what you don't want, e.g.
myEnclosingStackView.isHidden = true
}
In your table view controller, in the cellForRowAt indexPath func:
let cell = tableView.dequeueReusableCell(withIdentifier: "reuseIdentifier", for: indexPath)
// Configure the cell ..
if yourDataSource[indexPath.row].isCompletelyLoaded {
// do your fancy dynamic cell layout
} else {
// show the basic version (minus any dummy views)
cell.hideDummyViews()
}
return cell
You can choose your preferred method for hiding the items (isHidden for each view, removing, adjusting constraints). I prefer to embed any disappearing views in a stack view and then use isHidden = true on the enclosing stack. This keeps things organized in your storyboard/XIB file and neatly recalculates constraints for the hidden stacks.
It seems that you want to show some empty (or incomplete) cells until database content arrives and then you will reload each cell as you process new entries in the datasource. This answer will initially give you a set of cells appearing as per your storyboard/XIB, minus the hidden dummy elements. Then as items in your datasource are loaded fully, you can reload the cells.
By the way, it seems like a lot of work to carefully layout these dummy views for "visualization" and then never show them in the app. Why not have some user-friendly place holders or progress indicators showing and then animate in the real/dynamic views as the data arrives?
I assume you download the Image from FireBase and does't want the dummy Image to appear
(Why dont you declare an empty ImageView and an empty Label!). Try setting an Image array in the viewController. Or you can use a struct array if you want a Image and label text together.
var arrImage=[UIImage]()
var arrLblTxt=[String]()
In view did load append your Demo Image's if required.
arrImage.append(UIImage("Demo"))
arrLblTxt.append("Demo")
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tbl_AssignEmployees.dequeueReusableCellWithIdentifier("Cell", forIndexPath: indexPath) as YourCell
if arrImage[IndexPath.row] != UIImage("Demo")
{
cell.ImageView.Image = arrImage[indexPath.row]
cell.textLabel!.text = arrLblTxt[indexPath.row]
}
else
{
cell.ImageView.Image = nil
cell.textLabel!.text = arrLblTxt[indexPath.row]
}
cell.preservesSuperviewLayoutMargins = false
cell.separatorInset = UIEdgeInsetsZero
cell.layoutMargins = UIEdgeInsetsZero
return cell
}
Then After you download your Image From FireBase,
Reset your Image array with the new Images downloaded from fireBase
Then reload your tableView. tableView.reloadData
I'm having a UIImageview and few other components within a custom UITableviewcell, in a separate xib file, which I'm loading into my UITableview, which is in a UIView subclass and assigning image from URL dynamically.
Now the problem is, I want to resize the UIImageview and the Tableview row's height depending upon the size of image, obtained from the URL, dynamically. I got that working using the following code,
if let bpImageUrls = singleComponent["picture_link"]{
cell.newsImage.sd_setShowActivityIndicatorView(true)
cell.newsImage.sd_setIndicatorStyle(.gray)
cell.newsImage.sd_setImage(with: URL(string: bpImageUrls as! String))
let imageData = NSData(contentsOf:URL(string: bpImageUrls as! String)!)
let myImage = UIImage(data: imageData! as Data)
let aspectRatio = ( myImage?.size.height)! / ( myImage?.size.width)!
cell.imageHeightConstraint.constant = UIScreen.main.bounds.size.width * aspectRatio
}
It works like a charm, but the problem is, this will slow down the scrolling event of the UITableview, when I tried to do download the image asynchronously and update the view in the main thread, it's not working.
The view is tied with autolayout like in this image, I'd been struggling with this for a while now. Any insights will be helpful. Thanks.
When you load the image asynchronously the table view has already finished the layout for the cell. Just changing the cell's height constraint according to the image height does not change the height of the cell in the table view.
You have to make the table view recalculate the height of the cell after changing the height constraint. So when an image is loaded and the height constraint of the cell changed, tell the table view to reload that cell by calling:
if let indexPath = tableView.indexPath(for: cellWithImage) {
tableView.reloadRows(at: [indexPath], with: UITableViewRowAnimation.automatic)
}
I am using a UITableViewController to display rows of playing cards. I've set the Mode to "Aspect Fit" and I've checked the "Clip Subviews" in the Storyboard for the ImageView in each row's cell, its ContentView parent, and the Cell that contains the ContentView.
Everything looks as expected when the table is initially displayed but, when I swipe to scroll through the table, some (not all) of the new rows that scroll into view have images scaled to the wrong size, as shown.
It seems like if I drag quickly, I get more rows that are of the wrong size. If I drag slowly, all the new rows are scaled appropriately. When I use a different emulator, the incorrect scaling would make some of the images too big rather than too small and so the entire row of cards would not fit within the display.
In the code, I've set the contentMode and clipToBounds, but they do not seem to help:
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCellWithIdentifier("LineCell", forIndexPath: indexPath)
let index = indexPath.row % 4
let imagename = "cards\(index)"
let im = UIImage(named: imagename)
cell.contentMode = UIViewContentMode.ScaleAspectFit
cell.clipsToBounds = true
cell.contentView.contentMode = UIViewContentMode.ScaleAspectFit
cell.contentView.clipsToBounds = true
cell.imageView?.contentMode = UIViewContentMode.ScaleAspectFit
cell.imageView?.clipsToBounds = true
cell.imageView?.image = im
return cell
}
Did I miss something obvious? Thanks!
I ended up using a programmatic way around the problem.
let cellImage : UIImageView = UIImageView(frame: CGRectMake(0, 0, cell.bounds.size.width, cell.bounds.size.height))
cellImage.contentMode = UIViewContentMode.ScaleAspectFit
cellImage.clipsToBounds = true
cellImage.image = im
cell.addSubview(cellImage)
I would still like to find a way to do it via the Storyboard, which I recall used to work in a straightforward manner...back before Apple started coming out with devices of all shapes and sizes leading to their need to offer increasingly complicated ways to accommodate all the sizes in a semi-automated, constraint-based system that requires quite a bit of overhead even for the simplest tasks. But, for now, this will do.
I have been struggling this issue for 3 days and still can not figure it out. I do hope anyone here can help me.
Currently, i have an UITableView with customized cell(subclass of UITableViewCell) on it. Within this customized cell, there are many UILabels and all of them are set with Auto Layout (pining to cell content view) properly. By doing so, the cell height could display proper height no matter the UILabel is with long or short text.
The problem is that when i try to set one of the UILabels (the bottom one) to be hidden, the content view is not adjusted height accordingly and so as cell.
What i have down is i add an Hight Constraint from Interface Builder to that bottom label with following.
Priority = 250 (Low)
Constant = 0
Multiplier = 1
This make the constrain with the dotted line. Then, in the Swift file, i put following codes.
override func viewDidLoad() {
super.viewDidLoad()
//Setup TableView
tableView.allowsMultipleSelectionDuringEditing = true
//For tableView cell resize with autolayout
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 200
}
func tableView(tableView: UITableView, willSelectRowAtIndexPath indexPath: NSIndexPath) -> NSIndexPath? {
let cell = tableView.cellForRowAtIndexPath(indexPath) as! RecordTableViewCell
cell.lbLine.hidden = !cell.lbLine.hidden
if cell.lbLine.hidden != true{
//show
cell.ConstrainHeightForLine.priority = 250
}else{
//not show
cell.ConstrainHeightForLine.priority = 999
}
//tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
return indexPath
}
The tricky thing is that when i call tableView.reloadRowAtIndexPaths(), the cell would display the correct height but with a bug that it has to be trigger by double click (selecting) on the same row rather than one click.
For this, i also try following code inside the willSelectRowAtIndexPath method, but none of them is worked.
cell.contentView.setNeedsDisplay()
cell.contentView.layoutIfNeeded()
cell.contentView.setNeedsUpdateConstraints()
Currently the result is as following (with wrong cell Height):
As showed in the Figure 2, UILabel 6 could be with long text and when i hide this view, the content view is still showing as large as it before hiding.
Please do point me out where i am wrong and i will be appreciated.
I finally change the code
tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
to the following
tableView.reloadData()
Then, it work perfectly.
However, i don't really know the exactly reason on it. Hope someone can still comment it out.
I have a UICollectionView that holds a bunch of a photos.
However, if I scroll to the bottom the scrollview does not let me scroll to the bottom of the last few rows (it snaps back). I have tried override the collectionView.contentSize and just adding 1000 to the height but it doesn't fix the problem.
collectionView.contentSize = CGSizeMake(collectionView.contentSize.width, collectionView.contentSize.height + 1000)
Here is a video of the problem:
https://www.youtube.com/watch?v=fH57_pL0OjQ&list=UUIctdpq1Pzujc0u0ixMSeVw
Here is my code to create cells:
func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell
{
var cell = collectionView.dequeueReusableCellWithReuseIdentifier("selectPhotoCell", forIndexPath: indexPath) as B_SelectPhotoControllerViewCell
let reuseCount = ++cell.reuseCount
let asset = currentAssetAtIndex(indexPath.item)
PHImageManager.defaultManager().requestImageForAsset(asset, targetSize:_cellSize, contentMode: .AspectFit, options: nil)//the target size here can be set to CGZero for a super blurry preview
{
result, info in
if reuseCount == cell.reuseCount
{
cell.imageView.image = result
cell.imageView.frame = CGRectMake(0, 0,self._cellSize.width,self._cellSize.height)
}
}
return cell
}
private func currentAssetAtIndex(index:NSInteger)->PHAsset
{
if let fetchResult = _assetsFetchResults
{
return fetchResult[index] as PHAsset
}else
{
return _selectedAssets[index]
}
}
Update:
Because I am adding this as a child view controller, there seems to be some problems with the offsetting of the scrollview. I haven't fixed it yet but when open this view without adding it as a child view to another view controller, the scrollview is the correct size
The problem was I was adding this as a child view controller.
As a result, after doing some animations, the UICollectionView bounds were sizing to the view it was attached to. As a result its height was wrong and hence why it was getting cut off.
I just came across this question from a quick Google and felt I could add something useful.
I am running a segmentedControl with a UIView that changes to different UICollectionViews on the segment change and I couldn't get the collectionView to scroll fully down.
This may not be the solution for all, but I found that if I went to the XIB I was loading in the view and set size to freeform and decrease it by the size of a cell I had removed the problem.
suspect your CollectionView's bottomAchor was not set correctly to the parent uiview's safeAreaLayoutGuide bottomAnchor