14

In iOS 8, I'm trying to add a UIImageView as a subview of a UITextView, similar to what's shown here - but with the text below the image.

enter image description here

I want to do it using an exclusion path because on other devices, I might position the image differently depending on the screen size.

However there's a problem where if the CGRect used to create the exclusion path has a Y origin of 0, and takes up the full width of the textView, the exclusion fails and the text appears within exclusion path (so that the text is shown behind the imageView, as you can see in that screenshot).

To test this I built a simple app using the "single view" Xcode template, with the following:

- (void)viewDidLoad {
    [super viewDidLoad];

    // set up the textView
    CGRect frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
    UITextView *textView = [[UITextView alloc] initWithFrame:frame];
    [textView setFont:[UIFont systemFontOfSize:36.0]];
    [self.view addSubview:textView];
    textView.text = @"I wish this text appeared below the exclusion rect and not within it.";


    // add the photo

    CGFloat textViewWidth = textView.frame.size.width;

    // uncomment if you want to see that the exclusion path DOES work when not taking up the full width:
    // textViewWidth = textViewWidth / 2.0;

    CGFloat originY = 0.0;

    // uncomment if you want to see that the exclusion path DOES work if the Y origin isn't 0
    // originY = 54.0;

    UIImageView *imageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"photo_thumbnail"]];
    imageView.frame = CGRectMake(0, originY, textViewWidth, textViewWidth);
    imageView.alpha = 0.7; // just so you can see that the text is within the exclusion path (behind photo)
    [textView addSubview:imageView];


    // set the exclusion path (to the same rect as the imageView)
    CGRect exclusionRect = [textView convertRect:imageView.bounds fromView:imageView];
    UIBezierPath *exclusionPath = [UIBezierPath bezierPathWithRect:exclusionRect];
    textView.textContainer.exclusionPaths = @[exclusionPath];
}

I also tried subclassing NSTextContainer and overriding the -lineFragmentRectForProposedRect method, but adjusting the Y origin there doesn't seem to help either.

To use the custom NSTextContainer, I set up the UITextView stack like this in viewDidLoad():

// set up the textView stack
NSTextStorage *textStorage = [[NSTextStorage alloc] init];

NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager:layoutManager];

CGSize containerSize = CGSizeMake(self.view.frame.size.width, CGFLOAT_MAX);
CustomTextContainer *textContainer = [[CustomTextContainer alloc] initWithSize:containerSize];
[layoutManager addTextContainer:textContainer];

CGRect frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height);
UITextView *textView = [[UITextView alloc] initWithFrame:frame textContainer:textContainer];
[textView setFont:[UIFont systemFontOfSize:36.0]];
[self.view addSubview:textView];

textView.text = @"I wish this text appeared below the exclusion rect and not within it.";

Then I adjust the Y origin in the CustomTextContainer like this... but this fails just as spectacularly:

- (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(CGRect *)remainingRect {

    CGRect correctedRect = proposedRect;

    if (correctedRect.origin.y == 0) {
        correctedRect.origin.y += 414.0;
    }

    correctedRect = [super lineFragmentRectForProposedRect:correctedRect atIndex:characterIndex writingDirection:baseWritingDirection remainingRect:remainingRect];

    NSLog(@"origin.y: %f | characterIndex: %lu", correctedRect.origin.y, (unsigned long)characterIndex);

    return correctedRect;
}

I suppose this could be considered an Apple bug that I need to report (unless I'm missing something obvious), but if anybody has a workaround it would be much appreciated.

Jim Rhoades
  • 3,310
  • 3
  • 34
  • 50
  • I'm experiencing the same thing (iOS8). This seems like an Apple bug to me. If anyone figured-out a successful work-around, speak up! – Geoff H Jul 15 '15 at 11:29
  • Another workaround is to use storyboard layouts, and lay out the image, text, and constraints differently for different screen size classes. – Tim Nov 24 '15 at 01:39
  • No, it's not a bug. You just need to double check your exclusionPaths and make sure its rect is included by your textView.textContainer. @GeoffH, `textview.textContainerInset` and `textView.textContainer.lineFragmentPadding` and `textView.contentInset` need to be checked, too. – DawnSong Oct 18 '16 at 07:11

5 Answers5

7

This is an old bug.In the book Pushing the Limits iOS 7 Programming ,the author wrote this in page 377:

At the time of writing, Text Kit does not correctly handle some kinds of exclusion paths. In particular, if your exclusion paths would force some lines to be empty, the entire layout may fail. For example, if you attempt to lay out text in a circle this way, the top of the circle may be too small to include any text, and NSLayoutManager will silently fail. This limitation impacts all uses of NSTextContainer. Specifically, if lineFragmentRectForProposedRect:atIndex:writingDirection:remainingRect: ever returns an empty CGRect, the entire layout will fail.

Maybe you can override lineFragmentRectForProposedRect:atIndex:writingDirection:remainingRect: of your custom NSTextContainer to workaround.

wj2061
  • 6,778
  • 3
  • 36
  • 62
  • ...great... well I opted for a different approach to this problem, which comes with its own problems as well – CQM Nov 24 '15 at 16:02
4

I came across this problem as well. If you only need to exclude full width space at the top or bottom of the textView, you can use the textContainerInset.

Daniel
  • 475
  • 6
  • 10
2

Workaround suggestion:

Add a line break to your text.

textView.text = "\n" + textView.text

Apprentice
  • 49
  • 6
1

A quick workaround:

CGRect exclusionRect = [textView convertRect:imageView.bounds fromView:imageView];
if (exclusionRect.origin.x <= 0 && exclusionRect.origin.y <= 0 && exclusionRect.size.width >= textView.bounds.size.width) {
  exclusionRect.origin.x = 1;
  exclusionRect.origin.y = 1;
  exclusionRect.size.width -= 2;
}

Your image will still draw the same and unless you're using a font with glyphs that are 1px wide (I'm not even sure that's possible given kerning, etc), your exclusionRect will be guaranteed to be smaller than the full width.

I would be interested to know what kind of results you see if you allow your rect to be moved around in real-time. Attach a UIPanGestureRecognizer and update your exclusionRect as you pan around the screen. At what point does the text jump into the image?

Edit: If you're seeing problems until it is able to fit at least one character, maybe try adjusting your text frame.

if (exclusionRect.origin.x <= 0 && exclusionRect.origin.y <= 0 && exclusionRect.size.width >= textView.bounds.size.width) {
  frame.origin.y += CGRectGetMaxY(exclusionRect);
  frame.size.height -= CGRectGetMaxY(exclusionRect);
  [textView setFrame:frame];
}
Ian MacDonald
  • 13,472
  • 2
  • 30
  • 51
  • Unfortunately that doesn't seem to work either. It fails in the exact same way until you adjust the origin.x or origin.y to be large enough for characters to start showing along the left side or top (and is dependent on the font size). – Jim Rhoades Oct 29 '14 at 14:41
  • Tried adjusting the textView frame, but that pushes the imageView down as well. I also just tried adjusting the textContainer.size.height... but you can't because it's read only. An ugly workaround might be to have the imageView be a subview of self.view instead (along with adjusting the textView frame as you suggested) - and adjust the imageView's position when the textView scrolls. Not sure yet if I want to do that or not though. Anyways, I really appreciate the suggestions. – Jim Rhoades Oct 29 '14 at 15:13
  • Also tried adjusting the textView frame and then simply adjusting the imageView's Y origin to move it up (giving it a negative Y origin) - but there is blank white space where most of the image should be. – Jim Rhoades Oct 29 '14 at 15:23
0

I tried too hard toying with exclusion path.

My problem was that top level exclusion path never worked, it pushed content towards the top instead of center, while bottom content had double the exclusion path margin.

Here is what worked for me:

  1. Override UITextView
  2. Init it with your textcontainer by putting this inside init:

    super.init(frame: frame, textContainer: textContainer)

  3. As per Daniel's answer, put inside layoutSubviews:

    self.textContainerInset = UIEdgeInsets.init(top: t, left: l, bottom: b, right: r)

  4. Inside your UITextView subclass init() or in storyboard, disable scrolling.

    self.isScrollEnabled = false

For more details, read this super-helpful thread.

Nirav Bhatt
  • 6,940
  • 5
  • 45
  • 89