27

My app consists of an NSScrollView whose document view contains a number of vertically stacked NSTextViews — each of which resizes in the vertical direction as text is added.

Currently, this is all managed in code. The NSTextViews resize automatically, but I observe their resizing with an NSViewFrameDidChangeNotification, recalc all their origins so that they don't overlap, and resize their superview (the scroll view's document view) so that they all fit and can be scrolled to.

This seems as though it would be the perfect candidate for autolayout! I set NSLayoutConstraints between the first text view and its container, the last text view and its container, and each text view between each other. Then, if any text view grows, it automatically "pushes down" the origins of the text views below it to satisfy contraints, ultimately growing the size of the document view, and everyone's happy!

Except, it seems there's no way to make an NSTextView automatically grow as text is added in a constraints-based layout? Using the exact same NSTextView that automatically expanded as text was entered before, if I don't specify a constraint for its height, it defautls to 0 and isn't shown. If I do specify a constraint, even an inequality such as >=20, it stays stuck at that size and doesn't grow as text is added.

I suspect this has to do with NSTextView's implementation of -intrinsicContentSize, which by default returns (NSViewNoInstrinsicMetric, NSViewNoInstrinsicMetric).

So my questions: if I subclasses NSTextView to return a more meaningful intrinsicContentSize based on the layout of my text, would my autolayout then work as expected?

Any pointers on implementing intrinsicContentSize for a vertically resizing NSTextView?

jemmons
  • 18,605
  • 8
  • 55
  • 84
  • Could you add some graphic/screenshot/wireframe to facilitate understanding the setup please? Did you try to calculate the text height and return it in `intrinsicContentSize`? – JJD Oct 29 '12 at 10:54

4 Answers4

34

I am working on a very similar setup — a vertical stack of views containing text views that expand to fit their text contents and use autolayout.

So far I have had to subclass NSTextView, which is does not feel clean, but works superbly in practice:

- (NSSize) intrinsicContentSize {
    NSTextContainer* textContainer = [self textContainer];
    NSLayoutManager* layoutManager = [self layoutManager];
    [layoutManager ensureLayoutForTextContainer: textContainer];
    return [layoutManager usedRectForTextContainer: textContainer].size;
}

- (void) didChangeText {
    [super didChangeText];
    [self invalidateIntrinsicContentSize];
}

The initial size of the text view when added with addSubview is, curiously, not the intrinsic size; I have not yet figured out how to issue the first invalidation (hooking viewDidMoveToSuperview does not help), but I'm sure I will figure it out eventually.

Alexander Staubo
  • 3,148
  • 2
  • 25
  • 22
  • 1
    Note: I am currently trying to get the size to animate smoothly, and experimenting whether I can use autolayouts to control the height instead (or complementing the intrinsic size). It turns out you can modify a constraint by giving it a new height by calling `[constraint setConstant: newHeight]`, where `constraint` is a single constraint that provides the height setting. It works at the moment, but conflicts with `NSTextContainer`'s automatic resizing, resulting in flickering. – Alexander Staubo Jan 23 '13 at 12:01
  • An addendum to this, the height returned should also account for the textContainer inset, and also be ceil()'d to avoid fractional heights. Eg: NSSize size = [self.layoutManager usedRectForTextContainer:self.textContainer].size; size.width = NSViewNoIntrinsicMetric; size.height = ceil(size.height + self.textContainerInset.height * 2.0); return size; – seth Mar 10 '23 at 06:12
12

I had a similar problem with an NSTextField, and it turned out that it was due to the view wanting to hug its text content tightly along the vertical orientation. So if you set the content hugging priority to something lower than the priorities of your other constraints, it may work. E.g.:

[textView setContentHuggingPriority:NSLayoutPriorityFittingSizeCompression-1.0 forOrientation:NSLayoutConstraintOrientationVertical];

And in Swift, this would be:

setContentHuggingPriority(NSLayoutConstraint.Priority.fittingSizeCompression, for:NSLayoutConstraint.Orientation.vertical)
marcprux
  • 9,845
  • 3
  • 55
  • 72
  • So glad to have found this. I found that i had to do this for vertically-aligned `NSTextField`s in an `NSSplitView`, but only when they were selectable and not editable! So weird that the editing behavior effects constraints, but there you go. I changed the horizontal content-hugging priority for each of them from 250 to 249 and the problem went away. – theory Dec 03 '14 at 01:12
  • Under Swift 4 this seem to be `setContentHuggingPriority(NSLayoutConstraint.Priority.fittingSizeCompression, for:NSLayoutConstraint.Orientation.vertical)` not sure what the meaning of the -1 was but it seem to work without. Thanks, Marc! – Wizard of Kneup Jan 28 '18 at 18:45
6

Here is how to make an expanding NSTextView using Auto Layout, in Swift 3enter image description here

  • I used Anchors for Auto Layout
  • Use textDidChange from NSTextDelegate. NSTextViewDelegate conforms to NSTextDelegate
  • The idea is that textView has edges constraints, which means whenever its intrinsicContentSize changes, it will expand its parent, which is scrollView

    import Cocoa
    import Anchors
    
    class TextView: NSTextView {
      override var intrinsicContentSize: NSSize {
        guard let manager = textContainer?.layoutManager else {
          return .zero
        }
    
        manager.ensureLayout(for: textContainer!)
    
        return manager.usedRect(for: textContainer!).size
      }
    }
    
    class ViewController: NSViewController, NSTextViewDelegate {
    
      @IBOutlet var textView: NSTextView!
      @IBOutlet weak var scrollView: NSScrollView!
      override func viewDidLoad() {
        super.viewDidLoad()
    
        textView.delegate = self
    
        activate(
          scrollView.anchor.top.constant(100),
          scrollView.anchor.paddingHorizontally(30)
        )
    
        activate(
          textView.anchor.edges
        )
      }
    
      // MARK: - NSTextDelegate
      func textDidChange(_ notification: Notification) {
        guard let textView = notification.object as? NSTextView else { return }
    
        print(textView.intrinsicContentSize)
        textView.invalidateIntrinsicContentSize()
      }
    }
    
onmyway133
  • 45,645
  • 31
  • 257
  • 263
5

Class ready for copying and pasting. Swift 4.2, macOS 10.14

class HuggingTextView: NSTextView, NSTextViewDelegate {

    //MARK: - Initialization

    override init(frame: NSRect) {
        super.init(frame: frame)
        delegate = self
    }

    override init(frame frameRect: NSRect, textContainer container: NSTextContainer?) {
        super.init(frame: frameRect, textContainer: container)
        delegate = self
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        delegate = self
   }

    //MARK: - Overriden

    override var intrinsicContentSize: NSSize {
        guard let container = textContainer, let manager = container.layoutManager else {
            return super.intrinsicContentSize
        }
        manager.ensureLayout(for: container)
        return manager.usedRect(for: container).size
    }

    //MARK: - NSTextViewDelegate

    func textDidChange(_ notification: Notification) {
        invalidateIntrinsicContentSize()
    }

}
Mateusz Stompór
  • 461
  • 6
  • 15
  • I would return width as: `NSView.noIntrinsicMetric` and `invalidateIntrinsicContent` in a `layout()` call – Sentry.co Nov 22 '21 at 08:18