I'm have a table view and I'm using the tableView.dequeueReusableCellWithIdentifier to reuse the cells but still tableView is very slow.
and by slow, I mean it takes about 500 milliseconds to put 9 of my views in the tableView. and it's tested on apple A7 X64 processor so it must be pretty slower on older processors.
the reason that it's slow is because there are a few sub views and constraints.
but I've seen more complex tableCells with better performance, so there must be something I can do.
like caching a cell or something else??
any ideas?
sample code
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
tableView.registerNib(UINib(nibName: "ChatCell", bundle: nil), forCellReuseIdentifier: "ChatCell")
let cell = tableView.dequeueReusableCellWithIdentifier("ChatCell") as! ChatCell
return cell
}
the reason that it's slow is because there are a few sub views and constraints.
Personally, I don't suggest you use constraints in cell, especially when there're many subviews, it'll cost much CPU time and lead the scrolling lag. Instead, you can calculate manually based on cell frame.
And for more suggestion, i suggest you take time to read this post: Simple Strategies for Smooth Animation on the iPhone.
The call to registerNib is normally done only once in viewDidLoad, not every time you are asked for a cell in cellForRowAtIndexPath. Not sure how slow that call is, but it might be the reason for your slow response.
I think you are using effects (like shadow or round corners or etc) or having heavy calculations on UI
Edit: Code Sample added
//Add in your init func
tblView.registerClass(MSCustomVerticalListCell.self, forCellReuseIdentifier: NSStringFromClass(MSCustomVerticalListCell))
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = tblView.dequeueReusableCellWithIdentifier(NSStringFromClass(MSCustomVerticalListCell), forIndexPath: indexPath) as! MSCustomVerticalListCell
//add data binding
cell.item = dataSource[indexPath.row]
return cell
}
Your data binding class (Data Model):
class MSC_VCItem
{
var Title:String!
var Action:String!
var SubTitle:String!
var Icon:String!
init(title:String!,subTitle:String!,icon:String!,action:String!)
{
self.Title = title
self.SubTitle = subTitle
self.Icon = icon
self.Action = action
}
}
And Finally you custom table cell:
class MSCustomVerticalListCell : UITableViewCell {
let padding = 5
let imageWidth = 50
var customImageView: UIImageView!
var customTitleLabel: UILabel!
var customSubtitleLabel: UILabel!
var item: MSC_VCItem? {
didSet {
if let it = item {
customTitleLabel.text = it.Title
customSubtitleLabel.text = it.SubTitle
UIImage.loadFromCacheOrURL(it.Icon, callback: { (image: UIImage) -> () in
self.customImageView.image = image
})
setNeedsLayout()
}
}
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.backgroundColor = UIColor.clearColor()
customTitleLabel = UILabel(frame: CGRectZero)
self.addSubview(customTitleLabel)
customSubtitleLabel = UILabel(frame: CGRectZero)
contentView.addSubview(customSubtitleLabel)
customImageView = UIImageView(frame: CGRectZero)
customImageView.image = UIImage(named: "default")
contentView.addSubview(customImageView)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
//Write your UI here like bg color or text color
}
}
Related
I'm creating an app in UIKit, programmatically.
One screen has a UITableView with custom cells, some have sliders inside. All cells show up and can be selected. Other custom cell elements like buttons and text field react to interaction, but not the sliders.
Here's my code:
class CellWithSliderValues: UITableViewCell{
#IBOutlet var slider: UISlider! = {
let ctrl = UISlider()
// color setup here
return ctrl
}()
// some labels
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.addSubview(slider);
// layout code, skipped for StackOverflow
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
How the cell is created for the table, with tag calculation skipped:
let cell = mainScreen.table.dequeueReusableCell(withIdentifier: "CellWithSliderValues") as! CellWithSliderValues
cell.slider.maximumValue = Constants.BEST
cell.slider.minimumValue = Constants.WORST
cell.slider.value = currvalue
// tag is set here
cell.slider.addTarget(self, action: #selector(sliderChange), for: .valueChanged)
return cell
Part of the delegate that is called when clicking of the slider cell:
guard let cell = (mainScreen.table.cellForRow(at: indexPath) as? CellWithSliderValues) else {return}
cell.slider.becomeFirstResponder()
return
Finally, the function that should be called when the slider is dragged but is never actually called (seen with breakpoints):
#objc func sliderChange(sender: UISlider){
// get the values here
sender.resignFirstResponder()
}
The same approach did work with the text entry fields, so what am i doing wrong here? Is it because of the Slider not having delegates?
Commenter Sulthan's solution of adding a slider as a subview to the cell.contentView instead of the the cell directly worked like a charm.
self.contentView.addSubview(slider);
I wonder why didn't other cell elements require that.
So I am pretty new to iOS development. I try to create everything programmatically so my Storyboard is empty. I'm currently trying to get a TableView with custom cells. The TableView is running and looking fine when I use the standard UITableViewCell. I created a very simple class called "GameCell". Basically, I want to create a cell here with multiple labels and maybe some extra UIObjects in the future (imageView etc.). For some reason, the custom cells do not show up.
Game cell class:
class GameCell: UITableViewCell {
var mainTextLabel = UILabel()
var sideTextLabel = UILabel()
func setLabel() {
self.mainTextLabel.text = "FirstLabel"
self.sideTextLabel.text = "SecondLabel"
}
}
Here the additional necessary code to get the number of rows and return the cells to the TableView which I have in my ViewController. self.lastGamesCount is just an Int here and definitely not zero when I print it.
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.lastGamesCount
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: cellID) as! GameCell
In my viewDidLoad() I register the cells like this:
tableView.register(GameCell.self, forCellReuseIdentifier: cellID)
When I run everything the Build is successful I can see the navigation bar of my App and all but the TableView is empty. I go back to the normal UITableViewCell and the cells are showing up again. What am I missing here? Any help is appreciated.
Thanks!
The problem is you need to set constraints for these labels
var mainTextLabel = UILabel()
var sideTextLabel = UILabel()
after you add them to the cell
class GameCell: UITableViewCell {
let mainTextLabel = UILabel()
let sideTextLabel = UILabel()
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setLabel()
}
func setLabel() {
self.mainTextLabel.translatesAutoresizingMaskIntoConstraints = false
self.sideTextLabel.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(mainTextLabel)
self.contentView.addSubview(sideTextLabel)
NSLayoutConstraint.activate([
mainTextLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
mainTextLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
mainTextLabel.topAnchor.constraint(equalTo: self.contentView.topAnchor,constant:20),
sideTextLabel.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor),
sideTextLabel.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor),
sideTextLabel.topAnchor.constraint(equalTo: self.mainTextLabel.bottomAnchor,constant:20),
sideTextLabel.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor,constant:-20)
])
self.mainTextLabel.text = "FirstLabel"
self.sideTextLabel.text = "SecondLabel"
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
I'm trying to create a dynamic UITableView in a xib, so what I think might work is to put the table view inside a blank UIView. Then I would subclass this UIView and make it adhere to the protocols UITableViewDelegate and UITableViewDataSource. Can someone guide me with this, because I've tried many things, but none of them worked. Many Thanks!
EDIT:
I'll show you what I tried before (sorry, don't have the original code):
class TableControllerView: UIView, UITableViewDelegate, UITableViewDataSource {
let tableView = UITableView()
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
tableView.frame = self.frame
tableView.delegate = self
tableView.dataSource = self
}
func tableView(...cellForRowAt...) {
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
cell.titleLabel?.text = "test title"
return cell
}
}
but in this case the table view ended up being too big and not aligned with the view, even though I set it's frame to be the same as the view
EDIT 2 :
My code now looks like this:
class PharmacyTableView: UIView, UITableViewDelegate, UITableViewDataSource {
#IBOutlet weak var pharmacyTableView: UITableView!
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
//PROVISOIRE depends on user [medications].count
return 2
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = UITableViewCell(style: .subtitle, reuseIdentifier: nil)
cell.textLabel?.text = "text label test"
cell.detailTextLabel?.text = "detail label test"
return cell
}}
table view is initialized but only shows up when height anchor is constrained, which I don't want because it might grow or shrink depending on user data. I guess I'll be done after solving this?
P.S. : Also, thank you very much to the people that took the time to help me :)
EDIT 3:
So, I've changed the class of the table view to this:
class IntrinsicResizingTableView: UITableView {
override var contentSize:CGSize {
didSet {
self.invalidateIntrinsicContentSize()
}
}
override var intrinsicContentSize: CGSize {
self.layoutIfNeeded()
return CGSize(width: UIViewNoIntrinsicMetric, height: contentSize.height)
}
}
And now everything works fine! Finally!
Your question is unclear. Based on your final statement the following code is what you should use to keep the table view and its superview aligned.
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor).isActive = true
If this is not the answer you're looking for, please clarify your problem. Also it would help if you explain what you're doing this for? Why do you need this superview?
I have a cell class 'NewsCell' (subclass of UITableViewCell) that I use for two different kinds of news: OrganizationNews and ProjectNews. These news has common things, but some of elements are different. Namely, when my cell is used for ProjectNews I want to hide Organization's logo, when it is for OrganizationNews I want to hide Project's name button.
I have 'configureCell(_, forNews, ofProject)' method. I call it in 'NewsViewController'. I used 'removeFromSuperview' method, because I need to rearrange my elements in 'NewsCell'. Changing 'isHidden' value won't give me that effect.
So, that is the issue. I have 'Thread 1: Fatal error: Unexpectedly found nil while unwrapping an Optional value' exception in the lines projectNameButton.removeFromSuperview() or logoImageView.removeFromSuperview().
What should I do?
// NewsViewController.swift
func configureCell(_ cell: NewsCell, forNews news: News, ofProject project: Project? = nil) {
//...
if news is OrganizationNews {
cell.projectNameButton.removeFromSuperview()
} else if news is ProjectNews {
cell.logoImageView.removeFromSuperview()
}
// ...
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let news = newsCollection[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellIdentifiers.newsCell, for: indexPath) as! NewsCell
configureCell(cell, forNews: news)
cell.delegate = self
return cell
}
A UITableView or UICollectionView are built on the reuse concept, where the cells are reused and repopulated when you work on it.
When you try to call dequeReusableCell(withIdentifier:), it sometimes returns something that is created before. So, suppose you dequed before something which had all controls, then removed one (removeFromSuperview), then tried to deque again, the new dequed one may NOT have the subview.
I think the best solution for you is making two different cells.
Example:
class BaseNewsCell: UITableViewCell {
// Put the common views here
}
class OrganizationNewsCell: BaseNewsCell {
// Put here things that are ONLY for OrganizationNewsCell
}
class ProjectNewsCell: BaseNewsCell {
// Put here things that are ONLY for ProjectNewsCell
}
Then deque them from 2 different identifier by two different storyboard cells, xibs.
Or
class BaseNewsCell: UITableViewCell {
// Put the common views here
}
class OrganizationNewsCell: BaseNewsCell {
// This happens when this kind of cell is created for the first time
override func awakeFromNib() {
super.awakeFromNib()
someNonCommon.removeFromSuperview()
}
}
class ProjectNewsCell: BaseNewsCell {
override func awakeFromNib() {
super.awakeFromNib()
someOtherNonCommon.removeFromSuperview()
}
}
Note: This violates Liskov's principle (one of the SOLID principles), because you remove functionality from superclass in the subclass.
Change the removing lines as below,
if news is OrganizationNews {
cell.projectNameButton?.removeFromSuperview()
} else if news is ProjectNews {
cell.logoImageView?.removeFromSuperview()
}
This will fix the issue. But a good approach would be to create separate classes for each cell. You can create a base class to keep common logic there.
You shouldn't remove the subview from the outside of the cell. Let's refactor your code.
NewsCell.swift
final class NewsCell: UITableViewCell {
enum Kind {
case organization
case project
}
var logoImageView: UIImageView?
let nameLabel = UILabel()
var kind: NewsCell.Kind {
didSet {
if kind != oldValue {
setupLogoImageView()
self.setNeedsLayout()
}
}
}
init(kind: NewsCell.Kind, reuseIdentifier: String?) {
self.kind = kind
super.init(style: .default, reuseIdentifier: reuseIdentifier)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
// MARK: - Positioning
extension NewsCell {
override func layoutSubviews() {
super.layoutSubviews()
// Your layouting
switch kind {
case .organization:
// Setup frame for organization typed NewsCell
case .project:
// Setup frame for project typed NewsCell
}
}
}
// MARK: - Setup
extension NewsCell {
private func setupLogoImageView() {
logoImageView = kind == .organization ? UIImageView() : nil
}
}
How to use:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let news = newsCollection[indexPath.row]
var cell = tableView.dequeueReusableCell(withIdentifier: TableViewCellIdentifiers.newsCell) as? NewsCell
if cell == nil {
cell = NewsCell(kind: .organization, reuseIdentifier: TableViewCellIdentifiers.newsCell)
}
cell!.kind = news is Organization ? .organization: .project
return cell!
}
When we are creating customView, we set the view File's owner to custom class and we instantiate it with initWithFrame or initWithCode.
When we are creating customUITableViewCell, we set the view's class to custom class, instead File's owner's. And then register all the nibs so on.
İn this way, we always need to register the xibs to UIViewController and
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)so on.
What I find is that I don't want to register nibs all the time where I want to use customUITableViewCell. So I want to initialize xib inside my customUITableCell like the same process of creating customUIView. And I succeed. Here are the steps.
My question is what is the preferred way of creating customUITableCell?
With this method there is no need to register nibs and we can call customCell where we want to without loading/registering nib.
Set the view's File's Owner of xib to customUITableCell class. Not the view's class set to customClass, just File's Owner.
Image 1
My custom class called myView: UITableViewCell
import UIKit
class myView: UITableViewCell {
var subView: UIView!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initSubviews()
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
initSubviews()
}
func initSubviews(){
subView = Bundle.main.loadNibNamed("TableViewCell", owner: self, options: nil)?.first as! UIView
subView.autoresizingMask = UIViewAutoresizing(rawValue: UIViewAutoresizing.RawValue(UInt8(UIViewAutoresizing.flexibleWidth.rawValue) | UInt8(UIViewAutoresizing.flexibleHeight.rawValue)))
self.addSubview(subView)
}
}
İnside UIVivController, I did't register nibs and use
let cell = tableView.dequeueReusableCellWithIdentifier("reuseIdentifier", forIndexPath: indexPath)
Instead, I did this.
let cell = myView(style: .default , reuseIdentifier: "TableViewCell")
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var tableStyle: UITableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
tableStyle.frame = CGRect(x: 0, y: 0, width: self.view.frame.size.width, height: self.view.frame.size.height)
tableStyle.delegate = self
tableStyle.dataSource = self
view.addSubview(tableStyle)
}
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return 100.00
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 1 }
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = myView(style: .default , reuseIdentifier: "TableViewCell")
return cell
}
}
Here is the result.
Image 4
THANKS FOR YOUR TIME!!!
Your approach means every single time the UITableView requests a new cell, you're creating a brand new cell from scratch. That it means it has to:
find the nib
load the nib
parse it to find the views
make the views
update the cell
This is no better than having a long scroll view with custom views for it's entire length.
The beauty of UITableView is it optimizes much of this process and re-uses cells, massively cutting down the performance cost of having more cells than fit on your screen. With the traditional (correct) approach, steps 1-4 only have to happen once.
To expand on the differences in the xib:
When creating a cell with UITableView, you only give it the nib, and the system looks in the nib to find a UITableViewCell. A simple UIView will not work.
You actually can subclass the UIView in your xib with your custom class. It just happens that the norm is to use fileOwner, largely because that's the norm when using nibs with UIViewControllers as was required in the pre-storyboard era
An addition to the accepted answer:
If your only problem with the "classic" approach is that you need to register the nib and call dequeueReusableCell, you can simplify the calls with a nice protocol extension as discussed in this article:
protocol ReuseIdentifying {
static var reuseIdentifier: String { get }
}
extension ReuseIdentifying {
static var reuseIdentifier: String {
return String(describing: Self.self)
}
}
extension UITableViewCell: ReuseIdentifying {}
To register you just call
self.tableView.register(UINib(nibName: MyTableViewCell.reuseIdentifier, bundle: nil), forCellReuseIdentifier: MyTableViewCell. reuseIdentifier)
And to create it you call
let cell = self.tableView.dequeueReusableCell(withIdentifier: MyTableViewCell. reuseIdentifier, for: indexPath) as! MyTableViewCell
(Of course this only works if class, xib and reuse identifier all have the same name)