andygeers
andygeers

Reputation: 6976

Display partial lines of text on a deliberately truncated UITextView

There's many many questions on the internet complaining about how UITextView cuts off text prematurely, but I can't find any about the reverse situation where you actually want it to cut off the text before the end - but part way through a line.

It appears that UITextView will only ever render discreet lines of text - i.e. if it's not big enough, it will not render beyond the last line that fully fits inside it. I am seeking a way to make it render a partial line of text right up to the bottom boundary.

i.e. it currently does what's on the left here, whereas I am looking for a way to do what's on the right:

Illustration of partial lines of text

My UITextView has a fixed height and scrolling is disabled. I am assigning attributedText that could have bold, italics, etc rather than a simple string.

Upvotes: 0

Views: 374

Answers (2)

andygeers
andygeers

Reputation: 6976

OK, so it got ugly pretty fast, but I did manage to do this...

Rather than using a standard UITextView created through Interface Builder, I had to manually build one using a custom NSTextContainer:

let frame = containerView.frame

let textStorage = NSTextStorage(string: "")
let layoutManager = NSLayoutManager()

let textContainer = CardTextContainer(size: CGSize(width: frame.size.width, height: CGFloat.greatestFiniteMagnitude))
textContainer.widthTracksTextView = true

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

let textView = UITextView(frame: frame, textContainer: textContainer)
textView.isScrollEnabled = false

containerView.addSubview(textView)

I then defined CardTextContainer as a subclass of NSTextContainer that delegates its behaviour to the base class, except in the scenario where the line being rendered overlaps with the bottom of the NSTextView:

class CardTextContainer : NSTextContainer {

    override func lineFragmentRect(forProposedRect proposedRect: CGRect, at characterIndex: Int, writingDirection baseWritingDirection: NSWritingDirection, remaining remainingRect: UnsafeMutablePointer<CGRect>?) -> CGRect {

        let fragmentRect = super.lineFragmentRect(forProposedRect: proposedRect, at: characterIndex, writingDirection: baseWritingDirection, remaining: remainingRect)

        if ((fragmentRect.size.width > 0) || (fragmentRect.size.height > 0)) {
            // Just rely on the base implementation when it's working well
            return fragmentRect
        } else {
            // See if we could do slightly better than they did
            if (proposedRect.origin.y < self.size.height) {
                return CGRect(origin: proposedRect.origin, size: CGSize(width: self.size.width, height: proposedRect.size.height))
            } else {
                // Fall back to the 'I give up' solution
                return fragmentRect
            }
        }        
    }    
}

Upvotes: 0

DonMag
DonMag

Reputation: 77462

I don't think you can accomplish this with properties of UITextView.

One approach, though:

  • embed the text view in a "container" UIView
  • constrain the height of the container view as desired
  • set Clips to Bounds of the container view to TRUE

This will allow the text view to grow vertically to fit its text, but the container view will clip the portion you don't want visible.

Container view background is yellow, text view background is cyan:

enter image description here

Using Debug View Hierarchy - Show Clipped Content:

enter image description here

and here's the 3D view:

enter image description here


EDIT

Here is another approach -- may or may not work for your requirements...

  • UITextView - set position and width and height constraints
  • NOT editable
  • YES selectable
  • Scrolling Enabled - this will show the partial line
  • DISable show scroll indicators

Now, add a UIPanGestureRecognizer to that text view. It doesn't have to do anything... but it will (at least it appears to) prevent the text view from receiving the pan gesture, and thus prevents scrolling.

One noticeable issue though... if you have enough text that you get multiple lines, when you start selecting you can drag the selection handle down to the next line(s) and the text will scroll up... could probably prevent that in code, or maybe that would be desirable and then reset the scroll after the user has copied the selected text.

Upvotes: 1

Related Questions