I have a performance issue when scrolling vertical collection cells with horizontal collection view in it. The reason is cell's data reusing - horizontal collection view reloading on every parent cell reuse cycle. Does anybody know how can I avoid such amount of reloads ?
var items: [ProductItem] {
didSet {
collectionView.reloadData()
}
}
override func prepareForReuse() {
super.prepareForReuse()
items = nil
}
UICollectionView and UITableView use every cell again and again, that is how they work.
It you get your data from web ever time then cache them. You can use image-caching libraries.
Related
I have a TableView with a CollectionView inside.
So each Table cell is the delegate of the CollectionView.
The problem is that the collection View reloads on the main thread and the UITableView Cells are being reused.
That causes the collectionView cells to briefly present the labels of previous cells and then fade the changes.
How can I hide those labels when the tableView is initially passed through the return cell ?
The problem happens as collection.reload() is applied after the return cell of the UITableView in cell for row.
Here is a sample of the code.
UITableViewCell Code
let collectionDatasource = CollectionDataSource()
func configureCell(_ object: CustomClass) {
//Configure Object for tableView
self.presentedObject = object
self.nameLabel.text = object.name
//Set object and items on collection datasource
DispatchQueue.main.async {
let collectionItems = object.collectionItems ?? []
self.collectionDatasource.object = object
self.collectionDatasource.items = collectionItems
}
}
class CollectionDataSource: NSObject {
private var collectionView: UICollectionView?
var items: [CustomItem] = [] {
didSet {
DispatchQueue.main.async {
let changes = [CustomItem].changes(from: oldValue, to: self.items, identifiedBy: ==, comparedBy: <)
self.collectionView?.apply(changes: changes)
}
}
}
}
UICollectionView itself does its work asynchronously in contrast to UITableView. So, even if you will call reload collectionView directly in configureCell it will update the collection's cells after the table is set up (still can result in the mentioned issue). But you have added the "reload" call into DispatchQueue.main.async which makes the things worse.
You should skip using DispatchQueue.main.async if possible.
You should replace collectionView with a new one in configureCell. I suggest to add view into the cell of the same size and position as collection view and add collection view in the code. This will guarantee that previous collection view will never appear. 👍
I have UIViewController(HomeController) with horizontally scrollable UICollectionView in it. This collection view contains 2 cells (the number is not dynamic, there will be always only 2 cells). The cell covers all the screen. In each cell I have a child view controller, which has a UICollectionView itself and scrolls vertically. The cells of this inner collectonView contain big images (cover whole size of a cell).
I set up my child view controllers in HomeController's viewDidLoad like this:
addChild(viewController)
viewController.didMove(toParent: self)
To add child controller's view as a subview to my cells I have hostedView property in the cell's class:
class ContainerCell: UICollectionViewCell {
var hostedView: UIView? {
didSet {
guard let hostedView = hostedView else {
return
}
contentView.addSubview(hostedView)
hostedView.translatesAutoresizingMaskIntoConstraints = false
hostedView.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true
hostedView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true
hostedView.leftAnchor.constraint(equalTo: contentView.leftAnchor).isActive = true
hostedView.rightAnchor.constraint(equalTo: contentView.rightAnchor).isActive = true
}
}
}
This hostedView of the cell I set up in cellForItemAt method:
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = containerView.dequeueReusableCell(withReuseIdentifier: ContainerCell.reuseIdentifier, for: indexPath) as! ContainerCell
cell.hostedView = childControllers[indexPath.item].view
return cell
}
childControllers is an array where I store my two child view controllers.
The problem there is when app is launched for the first time and I scroll from the first cell to the second there is a delay in UI's reaction to the scroll. It scrolls not immediately but about 0.5 sec later after I do a scroll gesture. After that back and forth scrolls work fine. As far as I can tell this happens because at the launch the 2nd cell is not visible and cellForItemAt method for it hasn't been run yet. It runs when I scroll to this cell. This is completely fine behavior driven by iOS' reuse mechanism. But seems like because there is child controller in the cell with big images in it there is too much memory needed to be allocated so it causes this delay. At the launch Xcode shows about 100mb in memory, after a scroll to the second cell it grows to almost 200mb.
So even that I initialize all child controllers at viewDidLoad, the memory for them allocates only at cellForItemAt stage.
I would like to allocate that memory at the initial stage, so when I scroll they would be in the memory already. It kind of goes against cell reuse mechanism, but I know that there will always be only two cell (and two child controllers), so I don't really need that dynamic style behavior from my collectionView.
I tried to use different reuseIdentifiers for cells, and assign child controller's view to cell's hostedView in viewDidLoad, not in cellForItemAt, but it hasn't worked.
Is there any way to do that? And if there is not, how to approach this another way? This delay in UI reaction is really ruining user experience of the app.
I am developing a card game using UICollectionView. I override the draw() function of the UICollectionViewCell to arrange my images when load the cells. Every new game, i change the number of cards in the game. So the number and size of the cells change. I have to do all operations for UICollectionView in the viewWillAppear() method because of the third part library i use. My question is that how to reset all the works which done over the UICollectionView before. I want clear UICollectionView during each game without loading UIViewController again. Wanna do it in viewWillAppear() method. I want to clear UICollectionView because my images overlap if don't have clear UICollectionView in new game.
Note : I removed all the subviews from UICollectionView like below, but it didn't work
let subViews = gameCollectionView.subviews
if subViews != nil {
for view in subViews {
view.removeFromSuperview()
}
}
Clearing the collection is from clearing it's dataSource array like
arr = []
gameCollectionView.reloadData()
if you need to clear the cell then do it inside cellForRowAt
let cell = ///
cell.contentView.subviews//// clear here
or override
override func prepareForReuse() {
super.prepareForReuse()
// clear any subview here
}
if you correctly assign values for all properties of the collection cell every run of cellForRowAt without adding subviews , then you'll have no overlappings
You need to remove every subview from each cell's contentView, not from your collection view
gameCollectionView.visibleCells.forEach { $0.contentView.subviews.forEach { $0.removeFromSuperview() } }
But, I'm not sure for what you need to override draw method. You should rather create what you need just once, for example in awakeFromNib and then you should just change properties of your image views, etc. depending on content, for this you purpose you can use collection view's data source method cellForItemAt.
I've set up a UITableViewCell with UITableViewAutomaticDimension
The TableViewCell has a UICollectionView embedded in it which is not scrollable but can have a variable height based on the content of the collectionview.
Right now what I've tried is the render the cell and assign the height constraint of the collectionview to a variable collectionViewHeightConstraint and then update the height once the collectionview is rendered in the layoutSubviews method
override func layoutSubviews() {
super.layoutSubviews()
self.collectionViewHeightConstraint?.constant = self.collectionView!.contentSize.height
}
This is what the collectionview constraints look like (using cartography) :
self.contentView.addSubview(self.collectionview)
self.contentView.addSubview(self.holdingView)
constrain(self.holdingView!, self.contentView) {
holderView, container in
holderView.top == container.top
holderView.left == container.left
holderView.right == container.right
holderView.bottom == container.bottom
}
constrain(self.collectionView!, self.holdingView) {
collectionView, containerView in
collectionView.top == containerView.top
collectionView.left == containerView.left
collectionView.right == containerView.right
collectionViewHeightConstraint = collectionView.height == collectionViewHeight
containerView.bottom == collectionView.bottom
}
But that does not seem to update the cell height.
Is there any way to update the same?
Edit
This is not a duplicate question as suggested by some people and the explanation of why is in the comments below.
Since the comment was too small a space, I'll put everything here:
Note: You don't actually have to set the height constraint in viewDidLayoutSubviews just somewhere you can be sure that the UICollectionView has been set and your layout has been setup properly on your whole screen! For example, doing it in viewDidAppear and then calling layoutIfNeeded() will also work. Moving it into viewDidAppear will only work if you have your UICollectionView setup before viewDidAppear is called i.e you know your UICollectionView dataSource beforehand.
Fix 1:
Try reloading the UITableView after setting the height and checking if the heightConstant != contentSize. Use this to check if the height of the UICollectionView is updated properly i.e.:
override func layoutSubviews() {
super.layoutSubviews()
if self.collectionViewHeightConstraint?.constant != self.collectionView!.contentSize.height{
self.collectionViewHeightConstraint?.constant = self.collectionView!.contentSize.height
//to make sure height is recalculated
tableView.reloadData()
//or reload just the row depending on use case and if you know the index of the row to reload :)
}
}
I agree with your comment and that it is messy, I meant use that as a fix and/or to check if that is where the problem lies actually!
As for why it is 0, that happens probably because your UICollectionView hasn't been set yet (cellForItem hasn't been called yet) so contentSize isn't actually calculated!
Fix 2:
Once your dataSource for the UICollectionView has been set, that is you receive the data, you calculate the height the UICollectionView contentSize will have manually and set it once and reload the row. If the calculation is a tedious task, just set the dataSource and call reloadData on UICollectionView. This will ensure the UICollectionView is setup properly and then set the constraint of the cell to be the contentSize and call reloadData or reloadRow on the UITableView.
You basically can set the heightConstraint anytime after your UICollectionView has been setup and your view has been laid out. You just need to called tableView.reloadData() afterwards.
You can reload particular cell of tableview
let indexPath = IndexPath(item: rowNumber, section: 0)
tableView.reloadRows(at: [indexPath], with: .top)
Going by your requirement, I guess if we load the collectionview first and then load the tableview with the correct height of the collectionview, we can solve this.
collectionView.reloadData()
dispatch_async(dispatch_get_main_queue(), { () -> Void in
self.collectionViewHeightConstraint?.constant = self.collectionView!.contentSize.height
})
tableview.reloadData()
By this when tableview loads the cell has the desired height based on the collection view content size.
The Problem
I have a UITextView inside of a custom UITableViewCell subclass that is producing some odd behavior.
Here is a gif of the problem I'm seeing (relevant items are colored):
The height resizes correctly, but the full contents of the cell are not shown.
But when I pause execution and print out frames of the following:
Cell
Cell's content view
Text View
the frames are all correct!
Further, when I inspect the view using the view hierarchy debugger, the frames are all correct there too. There is one difference when using the view debugger though, and that is that I'm able to view the contents of the text view.
Here is what I see on the simulator vs in the debugger:
There seems to be an extraneous cell separator at the point where the yellow stops. Other than that, I can't find any sort of indicator of why the cell is not expanding past its original height.
My Code
In the storyboard, the UITextView has top, bottom and trailing constraints to the cell's content view, and a vertical spacing constraint to the UIImageView. The UIImageView has a fixed width and has a leading constraint to the cell's content view. I believe my constraints are set up correctly. Oh yeah, and scrolling is disabled on the UITextView.
As for code, I have a custom protocol that informs the table view controller when the notes field has changed:
protocol AddContactCellDelegate {
func notesViewDidChange(textView: UITextView)
}
Here is the relevant code from my UITableViewCell subclass:
class AddContactCell: UITableViewCell, UITextViewDelegate {
var delegate: AddContactCellDelegate?
func textViewDidChange(textView: UITextView) {
delegate?.notesViewDidChange(textView)
}
}
and from my UITableViewController subclass:
class AddContactViewController: UITableViewController, AddContactCellDelegate {
override func viewDidLoad() {
super.viewDidLoad()
tableView.estimatedRowHeight = 60.0
tableView.rowHeight = UITableViewAutomaticDimension
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let row = rows[indexPath.row]
let cell = tableView.dequeueReusableCellWithIdentifier(row.cellIdentifier, forIndexPath: indexPath) as! AddContactCell
cell.configureForRow(row)
cell.delegate = self
return cell
}
func notesViewDidChange(textView: UITextView) {
tableView.beginUpdates()
tableView.endUpdates()
}
}
Discussion
I've tried adding setNeedsLayout(), in to either the cell, the cell's content view, the tableview, or the textview, in just about every place, to no avail.
I rebuilt the entire view in the storyboard from scratch, same thing.
I tried creating a bare-bones project that has basically only the code above, and the sizing works correctly.
I've ruled out tons of other variables as being the culprit (for example, the tableview is in a container view, but the behavior persists without that).
One final weird point is that sometimes if I scroll the notes cell off the screen, leave the page, and come back again, everything looks as it should. The cell is fully resized and everything is visible. However, the problem resumes as soon as the text view goes to the next line, this time with all the previous text visible but none of the additional text.
If anyone has any ideas on what I might try next, it would be extremely helpful. Thanks!
Thanks to Steve asking for an example project that exhibits the behavior, I was able to figure out what is causing this issue.
To get the rounded corner effect, I had been using a layer mask on my cells. The mask uses the bounds of the cell to calculate its path, so when the cell updated itself to reflect the height of the text view, the mask was still in place and covered up the rest of the cell.
I didn't catch it because the rounding implementation was abstracted away in an extension. Whoops!