How do I force wrapping to isolate an attachment on a line?

I want to setup a UITextView so that image attachments get a line completely to themselves, without any text on the same line. This is trivial if the image is just as wide as the line, by simply ensuring the bounding box of the attachment returns the full width, but I haven't found a good way to do it when the image is less than full width.

Here is what I wish to achieve visually. https://www.dropbox.com/s/wktuxlod1rnyps1/Screen%20Shot%202020-06-29%20at%2015.14.26.png?dl=0

One way I have found that works OK, but feels like a hack to me, is inserting line breaks. I would much prefer to implement NSLayoutManager or NSTextAttachment methods to achieve this.

Another way to make it work is to return the full width from the NSTextAttachment bounding box method, but then just display the image in part of that area. The downside with this approach is that if you put the cursor at the end of the line, it doesn't sit next to the drawn image, but there is a gap and the cursor appears completely to the right.

Finally, I tried returning two different widths from the NSTextAttachment as the bounds: the first was the full line fragment width, to force a wrap, and on the next call I return the actual width of the image. This seems to give me most of what I want. The downside is that the next line of text will creep up to appear behind the image on the same line. I really need a way to force that text to the next line.

What is the sanctioned way to achieve this?

Accepted Reply

You can utilize the contextual information passed into the method. The method can return the wide (text container) width when asked for a position training other characters in the line to push the attachment out to the next line. It can return its natural size in other cases. In order to push out characters following the attachment, you can use -layoutManager:shouldBreakLineByWordBeforeCharacterAtIndex:.

Replies

You can subclass NSTextAttachment and override the method returning the layout bounds.
  • -attachmentBoundsForTextContainer:proposedLineFragment: glyphPosition: characterIndex:

Returning as wide as the text container width, you can fill the entire line.
Thanks. That is one of the things I already tried. You are right that this achieves the goal of having the attachment alone on the line.

The problem with that approach is that you then can't put the cursor behind the attachment. The cursor only goes right at the end of the line. I note that apps like Apple Notes can put the cursor behind the attachment (not at the end of the line), so I assume it is possible.


You can utilize the contextual information passed into the method. The method can return the wide (text container) width when asked for a position training other characters in the line to push the attachment out to the next line. It can return its natural size in other cases. In order to push out characters following the attachment, you can use -layoutManager:shouldBreakLineByWordBeforeCharacterAtIndex:.

The method can return the wide (text container) width when asked for a position training other characters in the line to push the attachment out to the next line. It can return its natural size in other cases.

This indeed works. It was one of the approaches I had tried. The problem is then the trailing text...

In order to push out characters following the attachment, you can use layoutManager:shouldBreakLineByWordBeforeCharacterAtIndex:.

I tried this, but it seems unrelated to the problem. This method seems to be about wrapping policy; it doesn't give you a way to actually force a wrap at any given character or word boundary, which is more what I need.

I also experimented with layoutManager(_:, shouldSetLineFragmentRect:, lineFragmentUsedRect:, baselineOffset:, in:, forGlyphRange:), but couldn't get that working either.

I wish there was just a method very similar to the attachment bounding rect one for standard text layout. Then it would be trivial. But I can't find a way to intervene in the text layout to cause it to wrap. I think inserting line breaks (which is what I did up until now) is probably the only way.

I apologize. You are quite correct that you can use layoutManager:shouldBreakLineByWordBeforeCharacterAtIndex: to get the trailing text to wrap. I just needed to "wrap" my head around what that method was doing.

For anyone wondering, the trick is to keep returning false from the delegate method until it passes in the word just after your attachment, at which point you return true. The layout manager will start with the last word on the line, and work back. By returning false, you are telling it you want to break earlier in the line. When you reach the point you actually want to break, you return true to indicate to make the wrap.
Spoke too soon, as it turns out. Using layoutManager:shouldBreakLineByWordBeforeCharacterAtIndex: is effective if the trailing text is longer than the remaining space on the line. In this case, it does what I described, and you can choose where to put the line break.

The problem is, if the trailing text is short, and fits on the line after the attachment, the layout manager doesn't even look for a line break. It isn't needed, and so there is no way to intervene.

Think inserting line break characters is the most foolproof approach.