objective-ccocoaswiftnsviewnslayoutmanager

Clarifying the behaviour of NSLayoutManager & NSTextContainer with respect to vertical layout?


Code Sample

I have a NSLayoutManager, NSTextContainer & NSTextStorage as properties in a custom NSView (not a TextView) initialized in awakeFromNib() as follows:

    textStorage = NSTextStorage(attributedString: self.attributedString)

    layoutManager = NSLayoutManager()
    textContainer = NSTextContainer(containerSize: NSMakeSize(self.frame.size.width, 1))

    layoutManager.addTextContainer(textContainer)
    textStorage.addLayoutManager(layoutManager)
    layoutManager.glyphRangeForTextContainer(textContainer)

The vertical containerSize of the NSTextContainer is deliberately set to 1 to see if it has the expected effect of hiding the text being rendered - it does not! It is making NO difference in the rendering of this text in the view - which is the subject of the question!

In drawRect I include the lines below to draw the text:

let glyphRange = layoutManager.glyphRangeForTextContainer(textContainer)
self.lockFocus()
layoutManager.drawGlyphsForGlyphRange(glyphRange, atPoint: NSMakePoint(0, 0))
self.unlockFocus()

Findings

  1. It appears best to work with your custom view in a flipped co-ordinate system like the NSTextView else I feel like I'm going to be in for a world of pain! Irrespective of this, NSLayoutManager always starts drawing its text internally in a flipped co-ordinate system (just like NSTextView)
  2. The containerSize.width property of NSTextContainer has the following effect: it is binding on a line level for all levels including the first (I know it's obvious but stick with me...)
  3. The containerSize.height property of NSTextContainer has a curve-ball: it will NOT be binding on the first line even if the containing view does not have the room to display it vertically BUT will be binding on subsequent lines

*it took me a long time to come to this hypothesis about containerSize.height because I was only drawing one line! *

Question

  1. Are my conclusions regarding NSTextContainer correct?
  2. What is the best way to control text drawing from a vertical perspective? I am wanting to place my single line of the text at the bottom of the view (and not have it floating at the top as in the default)

Solution

  • NSTextContainer has a containerSize property. The layout manager is laying out the text within that container. Presumably, the container is what is logically being positioned at (0, 0) in your view, but the text is laying out from its top. So, the container has slack.

    You could resize the container based on the rect returned from -[NSLayoutManager usedRectForTextContainer:] to size it to fit.


    Update:

    I think code like this in your drawRect() should work for what you want to achieve:

    layoutManager.ensureLayoutForTextContainer(textContainer)
    var rect = layoutManager.usedRectForTextContainer(textContainer)
    rect.origin.x += 2
    rect.origin.y = NSMaxY(self.bounds) - NSHeight(rect) - 4
    
    let glyphRange = layoutManager.glyphRangeForTextContainer(textContainer)
    layoutManager.drawGlyphsForGlyphRange(glyphRange, atPoint:rect.origin)