Since the upgrade to iOS 9, code that has been working fine before started to show really bad performance on scrolling.
I tried to create a minimal example from scratch leaving out most of the cell's configuration and various caching and content-generating code. Yet, the code is still not a quick read and the profiling is showing up differently :/
According to Apple Developer Forums (Edit: I found the link) this might result from how auto layout changed and having the layout heavy views in the back of it's superView made it less bad, still not good enough for release.
My suspicion is that the problem lies within UIKit
, especially in auto layout and UITextView
as the scrolling got instantly chunky as soon as I began setting cell.textView.text
.
Has anybody made similar observations? This didn't quite seem to come up as a problem for many.
Any suggestions to make this more smooth? Using UILabel
s fixes it, but there are to be links in attributed strings in real life that are to be tapable, so I see no way to avoid UITextView
.
I'm using custom UITableViewCell
subclasses, which I simplified as well, yet they are still quite a read because of the programmatic autolayout:
class AbstractRegularTimelineCell: UITableViewCell {
weak var iconView : UIImageView!
weak var headerLabel : UILabel!
weak var timeLabel : UILabel!
weak var regularContentContainer : UIView!
weak var contentHeightConstraint : NSLayoutConstraint!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit()
}
private func commonInit() {
self.selectionStyle = .None
// add the UI elements needed for all regular cells.
let iv = UIImageView(/*image: UIImage(named: "icon")*/)
iv.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(iv)
self.iconView = iv
iv.setContentCompressionResistancePriority(1000, forAxis: .Horizontal)
iv.setContentCompressionResistancePriority(1000, forAxis: .Vertical)
let hl = UILabel()
hl.numberOfLines = 0
hl.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(hl)
self.headerLabel = hl
hl.font = UIFont(name: "OpenSans-Semibold", size: UIFont.systemFontSize())
let tiv = UIImageView(/*image: UIImage(named: "timestamp")*/)
tiv.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(tiv)
let tl = UILabel()
tl.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(tl)
self.timeLabel = tl
tl.textColor = UIColor.lightGrayColor()
tl.font = UIFont(name: "OpenSans", size: UIFont.systemFontSize()-3)
let rcc = UIView()
rcc.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(rcc)
self.regularContentContainer = rcc
rcc.setContentCompressionResistancePriority(1000, forAxis: .Vertical)
rcc.setContentCompressionResistancePriority(100, forAxis: .Horizontal)
self.contentView.sendSubviewToBack(rcc) // this might help, according to Apple Dev Forums
// now, stitch the constraints together.
let views = ["iv":iv, "hl":hl, "tl":tl, "rcc":rcc, "tiv":tiv]
self.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|-(15)-[iv]-(15)-[hl]-(>=15)-|", options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: [:], views: views))
self.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(15)-[iv]", options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: nil, views: views))
self.contentView.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|-(17)-[hl]-(8)-[rcc]", options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: nil, views: views))
var otherConstraints = [NSLayoutConstraint]()
var c = NSLayoutConstraint(item: tiv, attribute: .Top, relatedBy: .Equal, toItem: rcc, attribute: .Bottom, multiplier: 1, constant: 8)
c.priority = 600
otherConstraints.append(c)
c = NSLayoutConstraint(item: tiv, attribute: .Top, relatedBy: .GreaterThanOrEqual, toItem: rcc, attribute: .Bottom, multiplier: 1, constant: 6)
otherConstraints.append(c)
c = NSLayoutConstraint(item: tl, attribute: .Leading, relatedBy: .Equal, toItem: tiv, attribute: .Trailing, multiplier: 1, constant: 6)
otherConstraints.append(c)
c = NSLayoutConstraint(item: tiv, attribute: .CenterY, relatedBy: .Equal, toItem: tl, attribute: .CenterY, multiplier: 1, constant: 1)
otherConstraints.append(c)
c = NSLayoutConstraint(item: self.contentView, attribute: .Bottom, relatedBy: .Equal, toItem: tiv, attribute: .Bottom, multiplier: 1, constant: 10)
otherConstraints.append(c)
c = NSLayoutConstraint(item: tiv, attribute: .Leading, relatedBy: .Equal, toItem: hl, attribute: .Leading, multiplier: 1, constant: 0)
otherConstraints.append(c)
c = NSLayoutConstraint(item: rcc, attribute: NSLayoutAttribute.Height, relatedBy: .LessThanOrEqual, toItem: nil, attribute: .NotAnAttribute, multiplier: 1, constant: 10000)
otherConstraints.append(c)
self.contentHeightConstraint = c
c = NSLayoutConstraint(item: self.contentView, attribute: .Trailing, relatedBy: .GreaterThanOrEqual, toItem: rcc, attribute: .Trailing, multiplier: 1, constant: 15)
otherConstraints.append(c)
c = NSLayoutConstraint(item: rcc, attribute: .Leading, relatedBy: .Equal, toItem: hl, attribute: .Leading, multiplier: 1, constant: 0)
otherConstraints.append(c)
self.contentView.addConstraints(otherConstraints)
}
}
class ExampleCell: AbstractRegularTimelineCell {
weak var textView : UITextView!
override required init(style: UITableViewCellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
self.setupTextView()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.setupTextView()
}
private func setupTextView() {
let t = UITextView()
t.translatesAutoresizingMaskIntoConstraints = false
self.regularContentContainer.addSubview(t)
self.regularContentContainer.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[t]|", options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: [:], views: ["t":t]))
self.regularContentContainer.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[t]|", options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: [:], views: ["t":t]))
self.textView = t
t.scrollEnabled = false
}
}
The less lengthy View Controller code:
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
weak var tableView : UITableView!
override func viewDidLoad() {
super.viewDidLoad()
let t = UITableView()
t.registerClass(ExampleCell.classForCoder(), forCellReuseIdentifier: "example")
t.translatesAutoresizingMaskIntoConstraints = false
self.view.addSubview(t)
self.tableView = t
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("H:|[t]|", options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: [:], views: ["t":t]))
self.view.addConstraints(NSLayoutConstraint.constraintsWithVisualFormat("V:|[t]|", options: NSLayoutFormatOptions.DirectionLeadingToTrailing, metrics: [:], views: ["t":t]))
t.delegate = self
t.dataSource = self
t.estimatedRowHeight = 350
}
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return 1
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return 300
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell : ExampleCell = tableView.dequeueReusableCellWithIdentifier("example", forIndexPath: indexPath) as! ExampleCell
cell.timeLabel.text = "probably never"
switch indexPath.row % 3 {
case 0:
cell.headerLabel.text = "First paragraph (Part one of two)"
cell.textView.text = "As any dedicated reader can clearly see, the Ideal of practical reason is a representation of, as far as I know, the things in themselves; as I have shown elsewhere, the phenomena should only be used as a canon for our understanding. These paralogisms of practical reason are what first give rise to the architectonic of practical reason."
case 1:
cell.headerLabel.text = "First paragraph, 2/2"
cell.textView.text = "As will easily be shown in the next section, reason would thereby be made to contradict, in view of these considerations, the Ideal of practical reason, yet the manifold depends on the phenomena. Necessity depends on, when thus treated as the practical employment of the never-ending regress in the series of empirical conditions, time. Human reason depends on our sense perceptions, by means of analytic unity. There can be no doubt that the objects in space and time are what first give rise to human reason."
default:
cell.headerLabel.text = "Second paragraph"
cell.textView.text = "Let us suppose that the noumena have nothing to do with necessity, since knowledge of the Categories is a posteriori. Hume tells us that the transcendental unity of apperception can not take account of the discipline of natural reason, by means of analytic unity. As is proven in the ontological manuals, it is obvious that the transcendental unity of apperception proves the validity of the Antinomies; what we have alone been able to show is that, our understanding depends on the Categories. It remains a mystery why the Ideal stands in need of reason. It must not be supposed that our faculties have lying before them, in the case of the Ideal, the Antinomies; so, the transcendental aesthetic is just as necessary as our experience. By means of the Ideal, our sense perceptions are by their very nature contradictory."
}
return cell
}
}