getting a NSTextField to grow with the text in auto layout?

后端 未结 5 1447
有刺的猬
有刺的猬 2020-12-05 08:42

I\'m trying to get my NSTextField to have its height grow (much like in iChat or Adium) once the user types enough text to overflow the width of the control (as asked on thi

相关标签:
5条回答
  • 2020-12-05 08:49

    This solution also works when setting the string value of the text field and when it's resized by AutoLayout. It just uses the attributed text property to calculate the intrinsic content size whenever it's needed.

    class AutoGrowingTextField: NSTextField {
        var maximumHeight: CGFloat = 100
    
        override var intrinsicContentSize: NSSize {
            let height = attributedStringValue.boundingRect(
                with: NSSize(width: bounds.width - 8, height: maximumHeight),
                options: [NSString.DrawingOptions.usesLineFragmentOrigin]
                ).height + 5
    
            return NSSize(width: NSView.noIntrinsicMetric, height: height)
        }
    
        override func textDidChange(_ notification: Notification) {
            super.textDidChange(notification)
            invalidateIntrinsicContentSize()
        }
    
        override func layout() {
            super.layout()
            invalidateIntrinsicContentSize()
        }
    }
    
    0 讨论(0)
  • 2020-12-05 08:52

    Solved it! (Inspired by https://github.com/jerrykrinock/CategoriesObjC/blob/master/NS(Attributed)String%2BGeometrics/NS(Attributed)String%2BGeometrics.m )

    Reading the Apple Documentation is usually helpful. Apple has engineered all this text layout stuff to be powerful enough to handle all sorts of complicated edge cases which is sometimes extremely helpful, and sometimes not.

    Firstly, I set the text field to wrap lines on word break, so we actually get multiple lines. (Your example code even had an if statement so it did nothing at all when wrapping was turned off).

    The trick to this one was to note that when text is being edited, it’s printed by a ‘field editor’ – a heavy weight NSTextView object, owned by an NSWindow, that’s reused by whatever NSTextField is currently the ‘first responder’ (selected). The NSTextView has a single NSTextContainer (rectangle where text goes), which has a NSLayoutManager to layout the text. We can ask the layout manager how much space it wants to use up, to get the new height of our text field.

    The other trick was to override the NSText delegate method - (void)textDidChange:(NSNotification *)notification to invalidate the intrinsic content size when the text is changed (so it doesn’t just wait to update when you commit changed by pressing return).

    The reason I didn’t use cellSizeForBounds as you originally suggested was I couldn’t solve your problem – even when invalidating the intrinsic content size of the cell, cellSizeForBounds: continued to return the old size.

    Find the example project on GitHub.

    @interface TSTTextGrowth()
    {
        BOOL _hasLastIntrinsicSize;
        BOOL _isEditing;
        NSSize _lastIntrinsicSize;
    }
    
    @end
    
    @implementation TSTTextGrowth
    
    - (void)textDidBeginEditing:(NSNotification *)notification
    {
        [super textDidBeginEditing:notification];
        _isEditing = YES;
    }
    
    - (void)textDidEndEditing:(NSNotification *)notification
    {
        [super textDidEndEditing:notification];
        _isEditing = NO;
    }
    
    - (void)textDidChange:(NSNotification *)notification
    {
        [super textDidChange:notification];
        [self invalidateIntrinsicContentSize];
    }
    
    -(NSSize)intrinsicContentSize
    {
        NSSize intrinsicSize = _lastIntrinsicSize;
    
        // Only update the size if we’re editing the text, or if we’ve not set it yet
        // If we try and update it while another text field is selected, it may shrink back down to only the size of one line (for some reason?)
        if(_isEditing || !_hasLastIntrinsicSize)
        {
            intrinsicSize = [super intrinsicContentSize];
    
            // If we’re being edited, get the shared NSTextView field editor, so we can get more info
            NSText *fieldEditor = [self.window fieldEditor:NO forObject:self];
            if([fieldEditor isKindOfClass:[NSTextView class]])
            {
                NSTextView *textView = (NSTextView *)fieldEditor;
                NSRect usedRect = [textView.textContainer.layoutManager usedRectForTextContainer:textView.textContainer];
    
                usedRect.size.height += 5.0; // magic number! (the field editor TextView is offset within the NSTextField. It’s easy to get the space above (it’s origin), but it’s difficult to get the default spacing for the bottom, as we may be changing the height
    
                intrinsicSize.height = usedRect.size.height;
            }
    
            _lastIntrinsicSize = intrinsicSize;
            _hasLastIntrinsicSize = YES;
        }
    
        return intrinsicSize;
    }
    
    @end
    

    As a last note, I’ve never actually used auto layout myself – the demos look amazing, but whenever I actually try it myself, I can’t get it to work quite right and it makes things more complicated. However, in this case, I think it actually did save a bunch of work – without it, -intrinsicContentSize wouldn’t exist, and you’d possibly have to set the frame yourself, calculating the new origin as well as the new size (not too difficult, but just more code).

    0 讨论(0)
  • 2020-12-05 08:59

    The solution of DouglasHeriot works great for me.

    Here is the same code on Swift 4

    class GrowingTextField: NSTextField {
    
        var editing = false
        var lastIntrinsicSize = NSSize.zero
        var hasLastIntrinsicSize = false
    
        override func textDidBeginEditing(_ notification: Notification) {
            super.textDidBeginEditing(notification)
            editing = true
        }
    
        override func textDidEndEditing(_ notification: Notification) {
            super.textDidEndEditing(notification)
            editing = false
        }
    
        override func textDidChange(_ notification: Notification) {
            super.textDidChange(notification)
            invalidateIntrinsicContentSize()
        }
    
        override var intrinsicContentSize: NSSize {
            get {
                var intrinsicSize = lastIntrinsicSize
    
                if editing || !hasLastIntrinsicSize {
    
                    intrinsicSize = super.intrinsicContentSize
    
                    // If we’re being edited, get the shared NSTextView field editor, so we can get more info
                    if let textView = self.window?.fieldEditor(false, for: self) as? NSTextView, let textContainer = textView.textContainer, var usedRect = textView.textContainer?.layoutManager?.usedRect(for: textContainer) {
                        usedRect.size.height += 5.0 // magic number! (the field editor TextView is offset within the NSTextField. It’s easy to get the space above (it’s origin), but it’s difficult to get the default spacing for the bottom, as we may be changing the height
                        intrinsicSize.height = usedRect.size.height
                    }
    
                    lastIntrinsicSize = intrinsicSize
                    hasLastIntrinsicSize = true
                }
    
                return intrinsicSize
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-05 09:05

    And if you want to limit the size of the TextField (e.g.):

    if (intrinsicSize.height > 100) 
    {
        intrinsicSize = _lastIntrinsicSize;
    } 
    else
    {
        _lastIntrinsicSize = intrinsicSize;
        _hasLastIntrinsicSize = YES;
    }
    

    (Diff)

    One thing I’m having trouble with is getting the NSTextField embedded in an NSScrollView and having it work properly (especially within an NSStackView). Going to look at whether it wouldn’t be easier with NSTextView instead.

    0 讨论(0)
  • 2020-12-05 09:08

    The solution by DouglasHeriot only works for fixed width text fields. In my app, I have text fields that I want to grow both horizontally and vertically. Therefore I modified the solution as follows:

    AutosizingTextField.h

    @interface AutosizingTextField : NSTextField {
        BOOL isEditing;
    }
    @end
    

    AutosizingTextField.m

    @implementation AutosizingTextField
    
    - (void)textDidBeginEditing:(NSNotification *)notification
    {
        [super textDidBeginEditing:notification];
        isEditing = YES;
    }
    
    - (void)textDidEndEditing:(NSNotification *)notification
    {
        [super textDidEndEditing:notification];
        isEditing = NO;
    }
    
    - (void)textDidChange:(NSNotification *)notification
    {
        [super textDidChange:notification];
        [self invalidateIntrinsicContentSize];
    }
    
    -(NSSize)intrinsicContentSize
    {
        if(isEditing)
        {
            NSText *fieldEditor = [self.window fieldEditor:NO forObject:self];
            if(fieldEditor)
            {
                NSTextFieldCell *cellCopy = [self.cell copy];
                cellCopy.stringValue = fieldEditor.string;
                return [cellCopy cellSize];
            }
        }
        return [self.cell cellSize];
    }
    @end
    

    There's a minor issue remaining: When typing spaces, the text jumps a bit to the left. However, that's not a problem in my app, because the text fields shouldn't contain spaces in most cases.

    0 讨论(0)
提交回复
热议问题