0

To summarize my problem: when I have many child views with widths equal to the width of their parent, where the parent's width is equal to their parent's width, there is a huge performance hit when the window is horizontally resized.


I am retrieving a large number of comments from an API and decided to create an individual NSView for each comment and then vertically space them all within an NSScrollView. Each individual comment grows to the height of the comment's text and matches the width of the container. This works fine when there aren't many loaded comments, but when the comments routinely exceed 100, there is a huge performance hit when the window is horizontally resized.

I create each comment's view:

var lastComment:NSView = documentView // The last view to vertically position around
for (index, comment) in commentsArray.enumerate() {
    var data = comment
    data["attributedString"] = attributedString
    let commentView = NSCommentView(data: data)
    documentView.addSubview(commentView)
    let verticalSpacing = (index == 0) ? 0 : 10 // Position the first comment without any top spacing 
    let secondAttribute = (index == 0) ? NSLayoutAttribute.Top : NSLayoutAttribute.Bottom
    let widthConstraint = NSLayoutConstraint(item: commentView, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: documentView, attribute: NSLayoutAttribute.Width, multiplier: 1, constant: 0)
    let topConstraint = NSLayoutConstraint(item: commentView, attribute: NSLayoutAttribute.Top, relatedBy: NSLayoutRelation.Equal, toItem: lastComment, attribute: secondAttribute, multiplier: 1, constant: verticalSpacing)
    documentView.addConstraints([widthConstraint, topConstraint])
    lastComment = commentView
    if index == commentsArray.count - 1{
        let bottom = NSLayoutConstraint(item: commentView, attribute: NSLayoutAttribute.Bottom, relatedBy: NSLayoutRelation.Equal, toItem: documentView, attribute: NSLayoutAttribute.Bottom, multiplier: 1, constant: 0)
        documentView.addConstraint(bottom)
    }
}

Where NSCommentView is defined as:

class NSCommentView: NSView {

    func setupFrame(){
        self.wantsLayer = true
        self.translatesAutoresizingMaskIntoConstraints = false
        self.layer?.backgroundColor = NSColor.redColor().CGColor
        // Set height of comment, fixed for testing
        let heightConstraint = NSLayoutConstraint(item: self, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: 50)
        self.addConstraint(heightConstraint)
    }

    func setupTextView(attributedString: NSAttributedString){
        let textView = NSTextView()
        textView.translatesAutoresizingMaskIntoConstraints = false
        textView.verticallyResizable = true
        textView.textStorage?.setAttributedString(attributedString)
        self.addSubview(textView)
        let heightConstraint = NSLayoutConstraint(item: textView, attribute: NSLayoutAttribute.Height, relatedBy: NSLayoutRelation.Equal, toItem: nil, attribute: NSLayoutAttribute.NotAnAttribute, multiplier: 1, constant: 50)
        textView.addConstraint(heightConstraint)

        // The performance hit occurs when I set a width equal to the parent width:
        let widthConstraint = NSLayoutConstraint(item: textView, attribute: NSLayoutAttribute.Width, relatedBy: NSLayoutRelation.Equal, toItem: self, attribute: NSLayoutAttribute.Width, multiplier: 1, constant: 0)
        self.addConstraint(widthConstraint)
    }

    convenience init(data:[String: AnyObject?]){
        self.init()
        setupFrame()
        if data["isComment"] as! Bool {
            let attributedString = data["attributedString"] as! NSAttributedString
            setupTextView(attributedString)
        }
    }
}

As you can see from the code, the issue is when I attempt to set the width of the NSTextView to the width of the NSCommentView. If I simply set it as a constant, there is no real performance degradation.

Is my view hierarchy not setup correctly, or am I overlooking something which is causing this performance issue?

Charlie
  • 11,380
  • 19
  • 83
  • 138
  • It seems like you're reinventing a table view. A nice feature of view-based `NSTableView`s is that it removes the views for off-screen cells. That will limit the total number and keep things responsive. You would have to profile using Instruments to know for sure, but I suspect the problem is that all of your text views are re-laying out their text for the new width. The ones which are scrolled off-screen don't know they don't have to do that, at least not immediately. Other experiments: use the empty string for all text views; set `textView.textContainer.widthTracksTextView` to false. – Ken Thomases Oct 20 '15 at 03:10
  • Oh, and don't use the `NS` prefix for your own classes. That's reserved for Cocoa classes. – Ken Thomases Oct 20 '15 at 03:11
  • @KenThomases thanks for the tip, I'll change my naming convention. Can I have an NSTableView setup with custom views? Part of my reason for picking a scroll view was because I figured I had to use the traditional text/column layout... – Charlie Oct 20 '15 at 13:28

1 Answers1

2

I agree with Ken that you're re-inventing the table view (and all it's associated optimizations). Is there an overriding reason to not use table view or collection view?

The performance hit is because when resizing horizontally you're re-calculating the size of every single subview! Not just the ones that are visible.

If you must do it this way. Some optimization attempts would be to:

  • Not use width, but to bind the leading and trailing edges to the superview.
  • Don't bind all subviews to the superview. Bind only the first to the superview, and all others to that first view.
  • Duplicate table/collectionview strategies and only update the constants for visible views. (this would be kind of silly though when you could just use one of those!)

I also noticed that you're fixing the height of the comment views right now. Once you enable that to flex, the performance will suffer even more since the views will have to resize across two dimensions, in realtime, impacting all the views below them on the page.

TL;DR - Save yourself a lot of trouble. Use a table or collection view.

Ben
  • 1,117
  • 13
  • 21
  • Could you provide an example of what I'm attempting with an NSTableView? – Charlie Oct 20 '15 at 19:10
  • Sure - can you post a picture of the layout, or is it just rows of text? – Ben Oct 20 '15 at 19:17
  • Of course: http://i.imgur.com/XEmNf3Y.png Also, I wanted to let you know that I have tried this, and got as far as making a custom class for the comment view, creating and assigning outlets for specific parts of the view, and implementing `numberOfRowsInTableView` and `objectValueForTableColumn`, however no data is actually showing up, hence me asking you for an example. – Charlie Oct 20 '15 at 20:09
  • Actually, I think I figured it out... Performance is definitely a lot better than trying to use an `NSScrollView`. Also, apparently an `NSCollectionView` might not be the way to go (http://stackoverflow.com/a/14670937/603986) – Charlie Oct 22 '15 at 00:35
  • Awesome! Glad you got it. – Ben Oct 22 '15 at 02:59