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.
Related
I have created a single prototype cell which has two labels (mainLabel and subLabel) and an uiimageview. In the uitableview I'd like to have several cells which reuse the prototype and when needed the subLabel is hidden and the uiimageview is changed with different one or with a uiswitch. The two labels have different text for each cell. Do you have any suggestions/hints in order to do it? possibly in a mvvm architecture?
I'll describe what I am doing:
I have a struct (the Model) with two properties: label and sublabel. This is then instantiate by a viewModel which provides text for each cell, done by a method called getModel(_ indexPath: IndexPath) -> cellModel { ... }. Finally in UIViewController, in tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) { ... } I am calling getModel(), using dequeueReusableCell and setting up each cell.
In getModel() there is a huuuuge switch which I use to know which cell is which
Then in uitableviewcell I have some method that hides sublabel and changes uiimageview.
It kind of works, however I have some issues with while scrolling. For example, sometimes a uiimageview is drawn in another cell, or a subLabel is hidden, even if it is not supposed to. I guess this is due because it is reusing the cell, and I am not resetting it.
Anyway, any suggestions or ideas?
I know this is overkilling...
No need for any pattern. Yes, you can use that single cell design for all cells. Just hide/empty label(s) and image view as you like per cell.
First of all you have to set default value to both the labels and imageview
i.e. (consider a title label, a sub label and a imageview)
lblTitle.isHidden = false
lblSubLabel.isHidden = false
imgViewIcon.image = nil
Then just show labels in specific condition that you want to match and set image in imageview
i.e. (consider your condition to hide sub label)
if needToHide == true {
lblSubLabel.isHidden = true
}
It seems like Apple's new feature of auto-flip interface on RTL languages cause problems when using UICollectionView.
I used constraints of type Trailing/Leading for the collection view and they switched their values, as they should, on RTL language.
The problem is that the data actually presented is of the last indexPath in the collection's data source but the UIScrollView.contentOffset.x of the first cell is 0.
A proper behaviour would have been one of the following:
Displaying the first indexPath correctly and switching the direction of the scroll (to the right) - Best option
Not flipping the UI/Constraints so the presented-data / indexPath / scrollView.contentOffset.x will be synchronised - Option that disabling the RTL support.
Presenting cell and data of the last indexPath but fixing the scrollView.contentOffset.x to represent the last cell position also.
I guess Apple might fix it sometime in the future but meanwhile we'll have to use workarounds like reversing array and/or scrolling to the last object.
I was in a similar situation and found a solution for this. If you are using swift, add the following snippet to your project, and it will make sure that the bounds.origin always follows leading edge of the collection view.
extension UICollectionViewFlowLayout {
open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
return true
}
}
If you are using Objective-C, just subclass the UICollectionViewLayout class, and override flipsHorizontallyInOppositeLayoutDirection, and return true. Use this subclass as the layout object of your collection view.
I am late but if you don't want to create an extension because it will affect all the collection View in our app. Simply create your own custom class ie.
class CustomLayoutForLocalization : UICollectionViewFlowLayout{
open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
return true
}
}
To use this class:
// your way of deciding on whether you need to apply this layout may vary depending on use of other tools like LanguageManager_iOS to handle multi-language support
if myCollectionView.effectiveUserInterfaceLayoutDirection == .rightToLeft {
let customLayout = CustomLayoutForRTL()
// if your elements are variable size use the following line
customLayout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
// if you want horizontal scroll (single line)
customLayout.scrollDirection = .horizontal
myCollectionView.collectionViewLayout = customLayout
}
There is one common solution for that problem that works for me, follow below steps to overcome that problem,
Give the auto layout constraint as per your requirement and then from attribute inspector change the semantic control property of the collection view to Force right-to-left from the storyboard.
Then open storyboard as source code and find for the “leading” attributes of your relevant collection view and replace that with the “left” and same for the “trailing” replace that with the “right”. Now you almost done.
now that will give you result as per your requirement.
import UIKit
extension UICollectionViewFlowLayout {
open override var flipsHorizontallyInOppositeLayoutDirection: Bool {
return UIApplication.shared.userInterfaceLayoutDirection == UIUserInterfaceLayoutDirection.rightToLeft
}
not pretty though simple math does the trick. (for horizontal collectionview)
- (void)switchSemanticDirection:(UISwitch*)sender {
//TEST switch the semantic direction between LTR and RTL.
if (sender.isOn) {
UIView.appearance.semanticContentAttribute = UISemanticContentAttributeForceLeftToRight;
} else {
UIView.appearance.semanticContentAttribute = UISemanticContentAttributeForceRightToLeft;
}
[self.myContent removeFromSuperview];
[self.view addSubview:self.myContent];
//reload your collection view to apply RTL setting programmatically
[self.list reloadData];
//position your content into the right offset after flipped RTL
self.list.contentOffset = CGPointMake(self.list.contentSize.width - self.list.contentOffset.x - self.list.bounds.size.width,
self.list.contentOffset.y);
}
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!
Hello
Now I've been trying to display html links in a UITableView.
I've been trying to do this via adding instances of UITableViewCell to the tableview's subview.
func updateViewController(){
for name : String in currentDirectoryNames {
var pusher = UITableViewCell();
pusher.textLabel?.text = name;
listView.addSubview(pusher);
}
}
Sadly the result ends up with the text overlapped in one row :(
It looks like this...
Any ideas?
That's not how UITableView works, at all. I highly suggest you read Apple's documentation on TableView Programming. At the very least, you'll need to set your view controller as the dataSource for the table view and implement the -tableView:numberOfRowsInSection: and -tableView:cellForRowAtIndexPath: methods.
The func you're calling is adding all of your subviews with origin.y equal to 0 (which makes them look like they're all on top of each other). You may have to do something like set pusher.frame.origin.y to dynamically calculate based on which cell is being added.
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.