Adding exclusion paths to multiple text views - ios

I'm trying to add multiple exclusion paths to a series of UITextViews laid out successively in a UIScrollView, like so:
while (lastRenderedGlyph < self.manager.numberOfGlyphs) {
CGRect textViewFrame = CGRectMake(currentXOffset, 10,
width / 2,
height - 20);
CGSize columnSize = CGSizeMake(CGRectGetWidth(textViewFrame) - 20,
CGRectGetHeight(textViewFrame) - 10);
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:columnSize];
[self.manager addTextContainer:textContainer];
UITextView *textView = [[UITextView alloc] initWithFrame:textViewFrame
textContainer:textContainer];
textView.scrollEnabled = NO;
textView.editable = NO;
textView.dataDetectorTypes = UIDataDetectorTypeAll;
textView.delegate = self;
textView.selectable = YES;
UIImageView *goat = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"goat"]];
[goat setContentMode:UIViewContentModeScaleAspectFit];
goat.frame = CGRectMake(0.0, 0.0, 50.0, 50.0);
[textView addSubview:goat];
[self.scrollView addSubview:textView];
textView.textContainer.exclusionPaths = #[[UIBezierPath bezierPathWithRect:CGRectMake(0.0, 0.0, 50.0, 50.0)]];
currentXOffset += CGRectGetWidth(textViewFrame);
lastRenderedGlyph = NSMaxRange([self.manager glyphRangeForTextContainer:textContainer]);
}
However, this causes the app to freeze up, and I've traced the issue to the setting of the exclusion path on each NSTextContainer. For example, if I set no exclusion paths, it works fine. Importantly, if I only set the exclusion path on the first NSTextContainer, then everything works just fine - but anything above one, and the app freezes. What am I doing wrong, or is this a bug?

I guess in retrospect this is obvious, but the solution I arrived at is to add exclusion paths to each new NSTextContainer right after you allocate the container but before doing anything else. i.e.
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:columnSize];
textContainer.exclusionPaths = #[exclusionPath];
[manager addTextContainer: textContainer];
rather than:
NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:columnSize];
[manager addTextContainer: textContainer];
textContainer.exclusionPaths = #[exclusionPath];
which causes the app to become unresponsive.
Simple when you know how. This only applies to multi-column, multi-page layouts where you're creating multiple text containers and views and breaking up the glyphs to fit properly. If you're only adding one exclusion path to one view, seems like you can add the exclusion path anywhere.

This is Very easy and helpful thing for ImageWrapping in RichText. but One should keep thing in Mind. a day ago I tried this one and found same code working differently for iOS 7.0 and iOS 7.1
The Blinking Cursor of TextView appears at different place until there is no text in UITextView.
I wrote this code.
UIBezierPath* exclusionPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 38, 21)];
_txtViewMessage.textContainer.exclusionPaths = #[exclusionPath];
and Found Following result in iOS 7.1 and iOS 7.0

Related

Duplicated UITextView doesn't show all text from the original

I'm trying to make a duplicate UITextView based on another UITextView for sharing purposes. This text view is added as a subview to viewToShare. There is an issue however, the duplicated text view doesn't show all of the original text from the text view I'm copying from. I'm using attributed text as well on the original text view, so I'm not sure if this is the issue. Setting the background color to black on textViewCopy showed me that the frame size is correct. For some reason it seems like new line \n characters from the original text view are causing havoc and preventing the text to be fully shown in the textViewCopy. I wonder if it's related to this question: NSAttributedString '\n' ignored
Screenshots:
Code:
- (UIView *)shareView
{
CGSize size = self.containerView.bounds.size;
UIView *viewToShare = [[UIView alloc]init];
viewToShare.backgroundColor = self.containerView.backgroundColor;
viewToShare.layer.cornerRadius = 6.0;
viewToShare.layer.masksToBounds = YES;
UITextView *textViewCopy = [[UITextView alloc]init];
textViewCopy.backgroundColor = [UIColor clearColor];
textViewCopy.tag = 1;
UIEdgeInsets textContainerInsets = self.textView.textContainerInset;
viewToShare.frame = CGRectMake(0, 0, size.width, size.height);
textViewCopy.frame = CGRectMake(0, 0, size.width, size.height);
textViewCopy.textContainerInset = textContainerInsets;
NSAttributedString *attributedStringCopy = [[NSAttributedString alloc]
initWithAttributedString:self.textView.attributedText];
textViewCopy.attributedText = attributedStringCopy;
[viewToShare addSubview:textViewCopy];
return viewToShare;
}
I figured out how to solve the issue based on this SO: Large Text Being Cut Off in UITextView That is Inside UIScrollView
- (UIView *)shareView
{
CGSize size = self.containerView.bounds.size;
UIView *viewToShare = [[UIView alloc]init];
viewToShare.backgroundColor = self.containerView.backgroundColor;
viewToShare.layer.cornerRadius = 6.0;
viewToShare.layer.masksToBounds = YES;
UITextView *textViewCopy = [[UITextView alloc]init];
textViewCopy.backgroundColor = [UIColor clearColor];
textViewCopy.tag = 1;
UIEdgeInsets textContainerInsets = self.textView.textContainerInset;
viewToShare.frame = CGRectMake(0, 0, size.width, size.height);
textViewCopy.frame = CGRectMake(0, 0, size.width, size.height);
// These two lines are needed to fix bug!
textViewCopy.scrollEnabled = NO;
textViewCopy.scrollEnabled = YES;
textViewCopy.textContainerInset = textContainerInsets;
NSAttributedString *attributedStringCopy = [[NSAttributedString alloc]
initWithAttributedString:self.textView.attributedText];
textViewCopy.attributedText = attributedStringCopy;
[viewToShare addSubview:textViewCopy];
return viewToShare;
}

UITextView textContainer exclusion path fails if full width and positioned at top of textContainer

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.
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.
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.
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.
Workaround suggestion:
Add a line break to your text.
textView.text = "\n" + textView.text
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];
}
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:
Override UITextView
Init it with your textcontainer by putting this inside init:
super.init(frame: frame, textContainer: textContainer)
As per Daniel's answer, put inside layoutSubviews:
self.textContainerInset = UIEdgeInsets.init(top: t, left: l, bottom: b, right: r)
Inside your UITextView subclass init() or in storyboard, disable scrolling.
self.isScrollEnabled = false
For more details, read this super-helpful thread.

multiple punch-out style mask?

I've done simple CALayer masks before but I think I'm getting confused on what they do. I'm trying to have a punch out effect with several (2) views.
Here's what I have so far. I'm looking to have a white square with punched out label and image (so you can see the brown background through it. Where am I going wrong?
- (void)viewDidLoad
{
[super viewDidLoad];
self.view.backgroundColor = [UIColor brownColor];
self.viewToPunch = [[UIView alloc] initWithFrame:CGRectZero];
[self.view addSubview:self.viewToPunch];
self.viewToPunch.backgroundColor = [UIColor whiteColor];
self.punchLabel = [[UILabel alloc] initWithFrame:CGRectZero];
self.punchLabel.textColor = [UIColor blackColor];
self.punchLabel.font = [UIFont boldSystemFontOfSize:20.0];
self.punchLabel.text = #"punch";
self.punchLabel.textAlignment = NSTextAlignmentCenter;
self.punchImage = [[UIImageView alloc] initWithImage:[[UIImage imageNamed:#"plus"] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
[self.punchImage setContentMode:UIViewContentModeCenter];
self.viewsToPunch = #[self.punchLabel,self.punchImage];
[self punch:self.viewToPunch withUIViews:self.viewsToPunch];
}
- (void)punch:(UIView *) viewToPunch withUIViews:(NSArray *)viewsToPunch
{
CALayer *punchMask = [CALayer layer];
punchMask.frame = viewToPunch.frame;
NSMutableArray *sublayers = [[NSMutableArray alloc] init];
for (UIView *views in viewsToPunch){
[sublayers addObject:views.layer];
}
punchMask.sublayers = sublayers;
punchMask.masksToBounds = YES;
viewToPunch.layer.mask = punchMask;
}
- (void)viewWillLayoutSubviews
{
[super viewWillLayoutSubviews];
self.viewToPunch.frame = CGRectMake(50, 50, 100, 100);
self.punchLabel.frame = CGRectMake(0, 0, 100, 100);
self.punchImage.frame = CGRectMake(0, 0, self.viewToPunch.frame.size.width, 40.);
[self punch:self.viewToPunch withUIViews:self.viewsToPunch];
}
So not only do the frames seem to be off, it seems to be the opposite of a punch out. How do I invert the mask and fix up the frames?
Thanks a lot for any help! I put it in a method punch:withUIViews: so I can hopefully reuse it in other areas.
When you apply a mask to a CALayer, it only gets drawn in the parts where the mask is not transparent. But you're simply applying an empty (transparent) mask, with the wrong coordinates (which is why your view isn't completely transparent: the mask isn't covering the view completely; it should be punchMask.frame = viewToPunch.bounds;)
You might want to look into CAShapeLayer and assign it a path. Use that as mask layer.
For example, see CAShapeLayer mask view or Getting Creative with CALayer Masks (cached blog post).
I tried to combine mask of CAGradationLayer and CAShapeLayer, and It is possible.
https://stackoverflow.com/a/32220792/3276863

Example of NSTextContainer with non regular shape?

Hi I'm working with the new TextKit API for iOS7 and I'm trying to produce a UITextView with an irregular shape. So far I have in a view controller:
-(void) loadView
{
self.view = [[UIView alloc] initWithFrame:CGRectMake(0,0,320,548)];
NSTextStorage *textStorage = [[NSTextStorage alloc] init];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[textStorage addLayoutManager: layoutManager];
BaseTextContainer *textContainer = [[BaseTextContainer alloc] initWithSize:CGSizeMake(100, 100)];
[layoutManager addTextContainer: textContainer];
BaseTextView *textView = [[BaseTextView alloc] initWithFrame:CGRectMake(110,124, 100, 100) textContainer:textContainer];
textView.backgroundColor = [UIColor blueColor];
textView.editable = YES;
[self.view addSubview:textView];
}
Then in my subclassed NSTextContainer, I want to have a mutablePath drawn as the shape of the text container, but not sure how to accomplish this. I have:
- (BOOL) isSimpleRectangularTextContainer
{
return NO;
}
- (void) drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
NSLog(#"TEST");
CGContextRef context = ctx;
CGSize layerSize = layer.frame.size;
CGAffineTransform transform = CGAffineTransformMakeScale(layerSize.width / self.initialSize.width, layerSize.height / self.initialSize.height);
CGMutablePathRef newGraphicMutablePath = CGPathCreateMutableCopyByTransformingPath(self.mutablePath, &transform);
CGContextAddPath(context, newGraphicMutablePath);
CGPathRelease(newGraphicMutablePath);
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextDrawPath(context, kCGPathFill);
}
Just a bit confused about how to get this to work. I cannot find an example anywhere of an NSTextContainer with an irregular shape.
There is no need for all that code constructing the Text Kit stack, as you are not modifying the architecture of the stack. Just start with a normal UITextView - let's say it's self.textView - and then assign one or more UIBezierPath objects to its exclusion paths:
self.tv.textContainer.exclusionPaths = myArrayOfBezierPaths;
These paths are exclusion paths, so for an ellipse you will want to make four paths, each one describing a corner of the text container.
Alternatively, you can build the Text Kit stack yourself so as to insert your own text container subclass, and modify where the text is allowed to go by overriding lineFragmentForProposedRect:, perhaps similar to what I do here: https://github.com/mattneub/Programming-iOS-Book-Examples/blob/master/bk2ch10p537exclusionPath2/ch23p813textKitShapes/MyTextContainer.swift
Some experiments:

Having difficulty wrapping text

I use the following code to add text in IOS
//Set up label frame
UILabel *tempLabel = [[UILabel alloc]initWithFrame:CGRectMake(100, 10, 210, 80)];
self.answer_text_label = tempLabel;
[tempLabel release];
[self.preview_answer_container addSubview:self.answer_text_label];
//Populate label with text
self.answer_text_label.text = self.answer.text;
self.answer_text_label.numberOfLines = 4;
self.answer_text_label.lineBreakMode = UILineBreakModeWordWrap;
[self.answer_text_label sizeToFit];
However, the result I get is as such, the text seems to overflow to the right instead of staying within the frame as stipulated in my label setup 'CGRectMake(100, 10, 210, 80)'
The wrapping works if I change to self.answer_text_label.numberOfLines = 0. But this will not work for me since I need to constrain the text within my stipulated label frame.
Any way I can wrap the text and keep to only 4 lines?
EDIT:
Try the suggested code
self.answer_text_label.text = self.answer.text;
[self.answer_text_label sizeToFit];
CGRect labelRect = self.answer_text_label.frame;
labelRect.origin.y = CGRectGetMaxY(labelRect);
labelRect.size = self.answer_text_label.frame.size;
self.answer_text_label.frame = labelRect;
result as follows. Did not seem to solve my problem
Try setting frame explicitly -
[self.answer_text_label sizeToFit];
CGRect labelRect = self.answer_text_label.frame;
labelRect.origin.y = CGRectGetMaxY(labelRect);
labelRect.size = self.answer_text_label.frame.size;
self.answer_text_label.frame = labelRect;
EDIT - Don't need to use this, just use following -
remove these of code just use below, no other property of frame, remove sizeToFit as well -
self.answer_text_label.numberOfLines = 4;
self.answer_text_label.lineBreakMode = UILineBreakModeWordWrap;
For vertical alignment - (With above line of code, use this as well, and do don't use size to fit)
CGSize textSize = [self.answer_text_label.text sizeWithFont:self.answer_text_label.font
constrainedToSize:CGSizeMake(self.answer_text_label.frame.size.width, MAXFLOAT)
lineBreakMode:self.answer_text_label.lineBreakMode];
self.answer_text_label.frame = CGRectMake(20.0f, 20.0f, textSize.width, textSize.height);
In iOS 6 and later, use NSLineBreakByWordWrapping, not UILineBreakModeWordWrap.
self.answer_text_label.lineBreakMode = NSLineBreakByWordWrapping;

Resources