TableView with Image SubView performance Issue - ios

Ive got one question about using a subView in my custom TableViewCells. On certain rows i want to one or more images, and i am doing this programmatically with:
func addImageToCell(image: UIImage, initialYCoordinate: CGFloat, initialHeight: CGFloat, initialXCoordinate: CGFloat,imageUUID: String) {
imageButton = UIButton.buttonWithType(UIButtonType.Custom) as? UIButton
.....
imageButton!.addTarget(formVC, action: "imageButtonPressed:", forControlEvents: .TouchUpInside)
self.contentView.addSubview(imageButton!)
}
That works fine. But, when i scroll my TableView and it comes to an row with an Image, there is a small "lag". Normally it is totally smooth, but when he is trying to load this cell, there is a (maybe 0,1 seconds) lag in the software.
Is there a better way to do this programmatically without any lags? Ill load my Images on initializing the UITableViewController from a CoreData Fetch.
This is my cellForRowAtIndexPath
if(cellObject.hasImages == true) {
cell.qIndex = indexPath.row
var initialXCoordinate:CGFloat = 344.0
let questionImages = formImages[cellObject.qIndex] as [String: Images]!
for (key,image) in questionImages {
let thumbnailImage = UIImage(data: image.image)
cell.addImageToCell(thumbnailImage, initialYCoordinate: 44.00 ,initialHeight: cellObject.rowheight + cellObject.noticeHeight!, initialXCoordinate: initialXCoordinate, imageUUID: image.imageUUID)
initialXCoordinate += 130
}
}
Any Ideas? Thanks in advance
Edit: Ill use this to prevent the Buttons to get reused:
override func prepareForReuse() {
super.prepareForReuse()
if(self.reuseIdentifier != "CraftInit") {
for item in self.contentView.subviews {
if(item.isKindOfClass(UIButton)) {
item.removeFromSuperview()
}
}
}

The lag is most probably caused by the allocation of new memory for UIButton, adding it to cell's contentView and loading an image into it at the time of the cell being reused. That's one problem.
The other problem is that you're creating a button per every reuse of the cell. Basically, every time you scroll the cell out of the visibility range and scroll it back - you add another button with another image, that also consumes memory and performance.
To make this work properly you need to create a custom table view cell with a maximum amount of images (buttons) pre-rendered and hide the ones you don't need to show.
Do not add / remove subviews while reusing the table view cell, hide and show them instead, it's much faster.

Where are you removing these subviews? Because if you're not removing them, as the user scroll, you'll keep on adding subviews to the same cell over and over again, degrading performance.
My approach usually is to to have such views constructed when the cell is created rather than in cellForRowAtIndexPath, but I'm guessing you can't do that as you don't know the number of images. That said, you could still formulate a strategy where these subviews are added at cell creation, and then reused as per your model.
Another suggestion is to have the UIImage objects created all at once, outside cellForRowAtIndexPath.

Related

Should I use dynamic stackview inside UITableViewCell or use UICollectionView?

Hello. I am building flights booking app, which has complex UITableViewCell. Basically, it is simple card with shadow, that has bunch of stackviews. First stackview, you see it on image is for labels. It is horizontal and dynamic. The next stackview shows flights. It has complex custom view, but for the sake of simplicity, it is shown with green border. It is also dynamic, so I need separate stackview for it. The next stackview is for airline companies that can handle this booking. I call them as operators. It is also dynamic, so I build yet another stackview for them. And of all these stack views are inside some core stackview. You can ask, why I created separate stack views instead of one? Because, labels above can be hidden. And also spacing in all stackviews are different.
It is really complex design. I followed above approach and build UITableViewCell. But performance is really bad. The reason is simple: I do too many stuff in cellForRowAt. The configure method of UITableViewCell is called everytime when the cell is dequeued. It means I should clean my stackview every time and after only that, append my views. I think it is really affects performance. I don't tell about other if/else statements inside cell. The first question is how can I increase scrolling performance of UITableViewCell in this case?
Some developers reckons that UITableView should be killed. UICollectionView rules the world. OK, but can I use UICollectionView with this design? Yes, of course, but above card would be one UICollectionViewCell and I simply don't avoid problem. The another solution is to build separate UICollectionViewCell for label (see on image), flight and operator. This would definitely increase performance. But, how can I make all of them live inside card?
P.S. What is inside my cellForRowAt method? There is only one configure method and assigning values to closure. But configure method is pretty complex. It gets some protocol which has bunch of computed properties. I pass implementation of that protocol to configure method. Protocol is like this:
protocol Booking {
var flights: [Flight] { get }
var operators: [Operator] { get }
var labels: [Label] { get }
var isExpanded: Bool { get set }
}
Implementation of this protocol is also complex. There are bunch of map functions and if/else statements. Some string manipulations. So, does that cause a problem? How can I solve it? By avoiding properties to be computed and just pass properties(flights, operators) to the implementation?
As I said in my comment, without seeing complete detail, it's tough to help. And, it's a pretty broad question to begin with.
However, this may give you some assistance...
Consider two cell classes. In each, the "basic" elements are added when the cell is created -- these elements will exists regardless of actually cell data:
your "main" stack view
your "labels" stack view
your "flights" stack view
your "operators" stack view
To simplify things, let's just think about the "operators" stack view, and we'll say each "row" is a single label.
What you may be doing now when you set the data in the cell is something like this...
In the cell's init func:
// create your main and 3 sub-stackViews
Then, when you set the data from cellForRowAt:
// remove all labels from operator stack
operatorStack.arrangedSubviews.forEach {
$0.removeFromSuperview()
}
// add new label for each operator
thisBooking.operators.forEach { op in
let v = UILabel()
v.font = .systemFont(ofSize: 15)
v.text = op.name
operatorStack.addArrangedSubview(v)
}
So, each time you dequeue a cell in cellForRowAt and set its data, you are removing all of the "operator" views from the stack view, and then re-creating and re-adding them.
Instead, if you know it will have a maximum of, let's say 10, "operator" subviews, you can add them when the cell is created and then show/hide as needed.
In the cell's init func:
// create your main and 3 sub-stackViews
// add 10 labels to operator stack
// when cell is created
for _ in 1...10 {
let v = UILabel()
v.font = .systemFont(ofSize: 15)
operatorStack.addArrangedSubview(v)
}
Then, when you set the data from cellForRowAt:
// set all labels in operator stack to hidden
operatorStack.arrangedSubviews.forEach {
$0.isHidden = true
}
// fill and unhide labels as needed
for (op, v) in zip(thisBooking.operators, operatorStack.arrangedSubviews) {
guard let label = v as? UILabel else { fatalError("Setup was wrong!") }
label.text = op.name
label.isHidden = false
}
That way, we only create and add "operator views" once - when the cell is created. When it is dequeued / reused, we're simply hiding the unused views.
Again, since you say you have a "really complex design", there is a lot more to consider... and as I mentioned you may need to rethink your whole approach.
However, the basic idea is to only create and add subviews once, then show/hide them as needed when the cell is reused.

Collapsible input form Swift 3

I am really getting disappointed that's why I ask this question, I found tutorials online for Collapsible TableView but they are with populating the cells with an array of Strings or something similar.
I want to make an Accordion like this with Swift 3,
for some days already I tried a lot of things with UITableViewController because apparently that's the only thing you can make collapsible if you want different cells.
Anyway I started to do it but as I asked my question here I cannot show different UIs in each Cell of each Section.
I am sure there's a way to do it, anyone has any suggestions how to make this work?
I thought maybe there's another way to do it (e.g. with ScrollView or something)
Here is how you could implement it with UIStackView:
Add a vertical UIStackView via storyboard
Add a UIButton as the first subview within the UIStackView
Add some UILabels as the second, third... subview within the UIStackView
Add an IBAction for the UIButton (the first subview)
Implement the IBAction like this:
#IBAction func sectionHeaderTapped(sender: UIButton) {
guard let stackView = sender.superview as? UIStackView else { return }
guard stackView.arrangedSubviews.count > 1 else { return }
let sectionIsCollapsed = stackView.arrangedSubviews[1].hidden
UIView.animateWithDuration(0.25) {
for i in 1..<stackView.arrangedSubviews.count {
stackView.arrangedSubviews[i].hidden = !sectionIsCollapsed
}
}
}
Create multiple UIStackViews like this (you can always use the same IBAction for the UIButton) and embed all of them in a parent (vertical) UIStackView to create a view like in your screenshot.
Feel free to ask if anything is unclear.
Result:

UITableView changing things of some cells in Swift

I have a UITableView with a custom UITalbeCellView. Every cell has a title a body and a date. When I fetch the data some of the titles must be red so there is a Bool that is set to true for those cells which title has to be red.
The first time the data is fetched it looks fine, but as you scroll up and down a few times all of the titles end up being red.
Im using an array of structures to store the data and on the cellForRowAtIndexPath there is an if and if the boolean in that position of the array is true I change the color to red:
let cell = tableView.dequeueReusableCellWithIdentifier("CommentsRowTVC", forIndexPath: indexPath) as! CommentsRowTVC
let single_comment = self.AOS[indexPath.row]
cell.titulo_comentario?.text = single_comment.titulo_comment
if single_comment.flag == true {
cell.titulo_comentario.textColor = UIColor.redColor()
}
Im gessing it reuses Views or something like that. Does this have any easy fix? Or do I have to implement my own TableView to prevent this from happening?
Im also thinking the method dequeueReusableCellWithIdentifier might be the one causing my problem... but Im new on swift and cant think of any replacement for that.
It is happening similar thing on buttons there are on the cells, when you click a button from a cell it changes its color and disables it, but just the one on that cell. However when clicking on a cell, buttons from other cells are also suffering those changes.
Since cells get reused you end up with cells that were previously red because they hadsingle_comment.flag == true and are now used for another row where there actually is single_comment.flag == false. You have to reset the color in the else branch again:
if single_comment.flag == true {
cell.titulo_comentario.textColor = UIColor.redColor()
} else {
cell.titulo_comentario.textColor = UIColor.whiteColor()
}
UITableViews reuse their cells, meaning that you will get cells inside your cellForRowAtIndexPath which have previously been used before, maybe have have set the textColor to red. Your job is it now to revert the color of the text to its original state.
In general whatever change you make inside the if-branch of cell-customization has to be undone in the else-branch. If you remove a label in the if, add it in the else.

UICollectionView does not scroll after it has been initialized

I have a subclass of UICollectionViewController that is nested inside a UINavigationController. The collection contains several cells (currently, 3) and each cell is as big as the full screen.
When the whole thing is shown, the collection view initally scrolls to a specific cell (which works flawlessly for each cell):
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let path = currentlyPresentedPhotoCellIndexPath { // this is set in the beginning
collectionView?.scrollToItemAtIndexPath(path, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: false)
}
}
However, the collection view refuses to scroll horizontally, hereafter, as if the user interaction was disabled. I am not sure what is happening, but this is what I have checked so far:
user interaction is enabled for the collection view
the next cell (right or left, depending on the scroll direction) is requested correctly which I found out by inspecting collectionView:cellForItemAtIndexPath:
the requested imagePath is the right one
scrollToItemAtIndexPath... does not work either if I try to trigger a scroll programmatically after everything has been loaded (nothing happens)
scrollRectToVisible... does neither
setting collectionView?.contentInset = UIEdgeInsetsZero before the programmatic scroll attempts take place does not change anything
the content size of the collection view is 3072x768 (= 3 screens, i.e. 3 cells)
Which bullet points are missing, here?
Although the post did not precisely tackle the root of my problem it forced me to ponder the code that I posted. If you look at it you will see that it basically says: Whenever the views need to be layouted, scroll to the cell at position currentlyPresentedPhotoCellIndexPath. However, and this you cannot see without any context, this variable is only set once, when the whole controller is being initialized. Thus, when you try to scroll, the layout changes, the controller then jumps back to the initial cell and it looks like nothing happens, at all.
To change this, you just have to enforce a single scroll, e.g. by doing this:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if let path = currentlyPresentedPhotoCellIndexPath { // only once possible
collectionView?.scrollToItemAtIndexPath(path, atScrollPosition: UICollectionViewScrollPosition.CenteredHorizontally, animated: false)
currentlyPresentedPhotoCellIndexPath = nil // because of this line
// "initiallyPresentedPhotoCellIndexPath" would probably a better name
}
}
A big thanks to Mr.T!

How to create UITableViewCells with complex content in order to keep scrolling fluent

I am working on a project where I have a table view which contains a number of cells with pretty complex content. It will be between usually not more than two, but in exceptions up to - lets say - 30 of them. Each of these complex cells contain a line chart. I am using ios-charts (https://github.com/danielgindi/ios-charts) for this.
This is what the View Controller's content looks like:
The code I use for dequeuing the cells in the viewController's cellForRowAtIndexPath method is kind of the following:
var cell = tableView.dequeueReusableCellWithIdentifier("PerfgraphCell", forIndexPath: indexPath) as? UITableViewCell
if cell == nil {
let nib:Array = NSBundle.mainBundle().loadNibNamed("PerfgraphCell", owner: self, options: nil)
cell = nib[0] as? FIBPerfgraphCell
}
cell.setupWithService(serviceObject, andPerfdataDatasourceId: indexPath.row - 1)
return cell!
and in the cell's code I have a method "setupWithService" which pulls the datasources from an already existing object "serviceObject" and inits the drawing of the graph, like this:
func setupWithService(service: ServiceObjectWithDetails, andPerfdataDatasourceId id: Int) {
let dataSource = service.perfdataDatasources[id]
let metadata = service.perfdataMetadataForDatasource(dataSource)
if metadata != nil {
if let lineChartData = service.perfdataDatasourcesLineChartData[dataSource] {
println("starting drawing for \(lineChartData.dataSets[0].yVals.count) values")
chartView.data = lineChartData
}
}
}
Now the problem: depending on how many values are to be drawn in the chart (between 100 and 2000) the drawing seems to get pretty complex. The user notices that when he scrolls down: as soon as a cell is to be dequeued that contains such a complex chart, the scrolling gets stuck for a short moment until the chart is initialized. That's of course ugly!
For such a case, does it make sense to NOT dequeue the cells on demand but predefine them and hold them in an array once the data that is needed for graphing is received by the view controller and just pull the corresponding cell out of this array when it's needed? Or is there a way to make the initialization of the chart asynchronous, so that the cell is there immediately but the chart appears whenever it's "ready"?
Thanks for your responses!
What you're trying to do is going to inevitably bump into some serious performance issues in one case or another. Storing all cells (and their data into memory) will quickly use up your application's available memory. On the other hand dequeueing and reloading will produce lags on some devices as you are experiencing right now. You'd be better off by rethinking your application's architecture, by either:
1- Precompute your graphs and export them as images. Loading images into and off cells will have much less of a performance knockoff.
2- Make the table view into a drill down menu where you only show one graph at a time.
Hope this helps!

Resources