Changing a views bounds affect the frame - ios

I'm trying to understand how a view responds to its bounds being changed. If I change the bounds origin of a view, will it change the frame origin accordingly?
E.g.
UIView *greenView = [[UIView alloc] initWithFrame:CGRectMake(150, 150, 150, 200)];
greenView.backgroundColor = [UIColor colorWithRed:0.494 green:0.827
blue:0.129 alpha:1];
[self.view addSubview:greenView];
greenView.bounds = CGRectMake(0, 150, greenView.bounds.size.width, greenView.bounds.size.height);
Would this not change the frame's origin to (150, 300)? Running the code above doesn't seem to change its frame. (I know you're not meant to change a views position using bounds, this is just a hypothetical).

Per Apple Documentation, here are the relationship between a view's frame, bounds and centre:
Although you can change the frame, bounds, and center properties
independent of the others, changes to one property affect the others
in the following ways:
When you set the frame property, the size value in the bounds property changes to match the new size of the frame rectangle. The
value in the center property similarly changes to match the new
center point of the frame rectangle.
When you set the center property, the origin value in the frame changes accordingly.
When you set the size of the bounds property, the size value in the frame property changes to match the new size of the bounds rectangle.
So, answer to your question, changing X,Y position on View's bounds should not affect frame. Most of the cases bounds starts with (0,0). Changing the height or width to negative values would allow origin of bounds to go negative.
EDIT: To answer OP question - No, changing the position of bounds won't affect frame in any way. Since bounds is with reference to view's own co-ordinate system, changing X,Y on self co-ordinate system would not change position in superview's co-ordinate system.
You can try by using two custom views like this:
UIView* view1 = [[UIView alloc] initWithFrame:CGRectMake(50.0f, 100.0f, 150.0f, 150.0f)];
view1.backgroundColor = [UIColor redColor];
NSLog(#"view1.bounds = %#", NSStringFromCGRect(view1.bounds));
NSLog(#"view1.frame = %#", NSStringFromCGRect(view1.frame));
UIView* view2 = [[UIView alloc] initWithFrame:CGRectInset(view1.bounds, 20.0f, 20.0f)];
view2.backgroundColor = [UIColor yellowColor];
NSLog(#"view2.bounds = %#", NSStringFromCGRect(view2.bounds));
NSLog(#"view2.frame = %#", NSStringFromCGRect(view2.frame));
NSLog(#"view1.bounds = %#", NSStringFromCGRect(view1.bounds));
NSLog(#"view1.frame = %#", NSStringFromCGRect(view1.frame));
NSLog(#"view2.bounds = %#", NSStringFromCGRect(view2.bounds));
NSLog(#"view2.frame = %#", NSStringFromCGRect(view2.frame));
[view1 addSubview:view2];
And then change the subview bound like this:
CGRect frame = view2.bounds;
frame.origin.x += 20.0f;
frame.origin.y += 20.0f;
view2.bounds = frame;
Changing the bounds would not impact frame at all. And both the views would look same on screen:
And finally, try by changing the bounds of parent view to see below screen:
CGRect frame = view1.bounds;
frame.origin.x += 20.0f;
frame.origin.y += 20.0f;
view1.bounds = frame;

Related

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.

Reduce a UIView's Height From the Top Down?

Resizing a UIView's height can be done by modifying it's frame.size.height property OR by modifying it's NSLayoutHeight constraint. However, by default, reducing a UIView's height will cut off the UIView from the bottom up, I need to reduce a UIView's height from the top down. Is this possible?
Adjust the origin.y of the view as you change the height.
CGFloat const delta = 100;
[aView setFrame:CGRectMake(CGRectGetMinX([aView frame]), CGRectGetMinY([aView frame]) + delta, CGRectGetWidth([aView bounds]), CGRectGetHeight([aView bounds]) - delta)];
You will need to adjust the origin of the frame as well.
Eg:
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 20.0, 40.0)];
to change the height to 30.0 from the 'top down' do:
view.frame = CGRectMake(0.0, 10.0, 20.0, 30.0);
Create container UIView with clipsToBounds == YES, and move your view inside the container view.
Just like clipping mask in drawing software.

UIImageView and autolayout

I have a view that is set up nicely using autolayout. The view contains a series of labels stacked from top to bottom. I am allowing the intrinsic size of these labels to determine the size of the view.
The final step is to add a background from an image. I started by trying the colorWithPatternImage method on UIColor but this isn't quite what I am looking for. I do not want to tile the image, and I can not guarantee it will always be larger than the intrinsic size of the view.
Similarly, adding a uiImageView to the view itself doesn't quite work. The view will expand to accommodate the image when I want to keep the intrinsic size based on the labels.
I guess what I am looking for is the following.
1) The background should have no effect on the size of the view.
2) The image should be scaled to fill the view but in it's original aspect ration (so cropping edges if necessary).
Any ideas appreciated.
In my case, I needed it for a UIImageView inside a dynamically-sized view in a UITableViewCell, but the image refused to shrink below its instristic size and instead worked as a minimum-size constraint for the superview. The only way I could get it ignore the intristic size is by lowering the priority at which it is enforced, right after creating the cell:
[imageView setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisHorizontal];
[imageView setContentCompressionResistancePriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisVertical];
After this, all my constraints magically started working. In the OP's case, setting UIViewContentModeScaleAspectFill is also required, as per Mundi's answer.
In Interface Builder, add a UIImageView as the first subview to the view. Make sure its size always matches the view.
Then, in Interface Builder or code, set the contentMode:
backgroundImageView.contentMode = UIViewContentModeScaleAspectFill;
Here's how I would approach this. Hopefully it helps. :)
CGRect contentFrame = CGRectMake(0, 0, self.view.frame.size.width, 0); // This will be the frame used to create the background image view.
UIEdgeInsets contentInsets = UIEdgeInsetsMake(20, 20, 20, 20); // The margins by which the labels will be inset from the edge of their parent view.
CGFloat labelHeight = 21;
CGFloat verticalGap = 8; // The vertical space between labels
CGFloat y = contentInsets.top;
int numberOfLabels = 10;
for (int i = 0; i < numberOfLabels; i++) {
CGRect frame = CGRectMake(contentInsets.left, y, self.view.frame.size.width - (contentInsets.left + contentInsets.right), labelHeight);
UILabel *label = [[[UILabel alloc] initWithFrame: frame] autorelease];
// customize the label here
[self.view addSubview: label];
contentFrame = CGRectUnion(contentFrame, label.frame);
y += labelHeight + verticalGap;
}
contentFrame.size.height += contentInsets.bottom;
UIImageView *backgroundImageView = [[[UIImageView alloc] initWithFrame: contentFrame] autorelease];
[backgroundImageView setClipsToBounds: YES];
[backgroundImageView setContentMode: UIViewContentModeScaleAspectFill];
[backgroundImageView setImage: [UIImage imageNamed: #"background_image.png"]];
[self.view insertSubview: backgroundImageView atIndex: 0];

UIScrollView zooms subviews when increasing subview dimensions

I have a UIScrollView that contains a view derived from UIView that uses CATiledLayer. Essentially, in my ViewController viewDidLoad:
_tiledView = [[TiledView alloc] initWithFrame:rect tileSize:_tileSize];
_scrollView = [[ScrollingView alloc] initWithFrame:rect];
_scrollView.contentSize = _tiledView.frame.size;
_scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_scrollView.decelerationRate = UIScrollViewDecelerationRateFast;
_scrollView.scrollEnabled = YES;
_scrollView.delegate = self;
[_scrollView addSubview:_tiledView];
Initially, _tiledView is a 4x4 grid of 256x256 tiles. I'm trying increase the dimensions of _tiledView at runtime. When constructing _tiledView, I simply compute the size of the view by multiplying the number of tiles by its size. Then I set the size of _tiledView.frame and _tiledView.bounds, e.g.:
CGRect frame = self.frame;
frame.origin = CGPointZero;
frame.size = CGSizeMake(tileSize.width*4, tileSize.height*4);
self.frame = frame;
self.bounds = frame;
Now, when I hit a button in the interface, all I want to accomplish as a first step is increasing the dimensions of _tiledView by one 256x256 tile to both right and bottom. This is what I attempted:
- (void)addTiles:(id)sender
{
CGRect rect = _tiledView.frame;
rect.size.width += _tileSize.width;
rect.size.height += _tileSize.height;
_tiledView.frame = rect;
_tiledView.bounds = rect;
CGSize size = _scrollView.contentSize;
size.width += _tileSize.width;
size.height += _tileSize.height;
_scrollView.contentSize = size;
[_scrollView setNeedsLayout];
}
However, this doesn't work as expected. What happens is that _tiledView gets bigger as if it had been zoomed in - same number of tiles as the beginning though, i.e. 4x4. I checked the _scrollView.contentsScaleFactor property and it says 1.0. I assume _scrollView did not technically zoom the contents in.
I was expecting _tileView to stay put in its current place in the interface but add 9 new tiles, i.e. 4 to the right, 4 at the bottom and 1 at the bottom-right corner. Instead, the initial 16 tiles got bigger to fill in the space that could have been filled by 25 tiles.
What am I missing? What am I doing wrong? Any help would be appreciated.
In case anyone finds it useful. After digging further, I realized that I the contentMode defaults to ScaleToFill. So I set it to:
_tiledView.contentMode = UIViewContentModeRedraw;
upon initialization. And adjusted addTiles like this:
CGRect rect = _tiledView.frame;
rect.size.width += _tileSize.width;
rect.size.height += _tileSize.height;
_tiledView.frame = rect;
_tiledView.bounds = rect;
_scrollView.contentSize = rect.size;
[_tiledView setNeedsDisplay];
And, that had the effect I was looking for.

When would a UIView's bounds.origin not be (0, 0)?

When would an UIView's bounds.origin not be (0, 0)?
This paragraph was helpful to me:
IMPORTANT!! Bounds X and Y, the origin, are for moving inside the
View. For eample X+5, moving 5pix to the left of the frame's origin
meaning draw all content within this View to the left 5pix of frame's
origin. It doesn't do anything to itself, it is what being drew on it
that get affected.
But it describes only the case when I had set the value of bounds.origin myself.
In what other cases the value of bounds.origin != (0, 0)?
View's frame determines its location in superview. View's bounds determines its subviews locations. That means, if you change view's bounds, its location won't be changed, but all of its subviews location will be changed.
Positive width and height is like you draw a view from upper-left to bottom-right, while negative value is from bottom-right to upper-left. So
frame1 = CGRectMake(100, 100, -50, -50)
is totally identical with
frame2 = CGRectMake(50, 50, 50, 50).
And in fact, if you init a view with frame1, it will AUTOMATICALLY CHANGED to frame2.
But the bounds.origin of the views are not identical. Bounds.origin indicates the point that you "draw" the view, so all subviews frames will originate at this point.
For example, in Landscape iPhone 6, we have:
UIView *leftView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 275, 275)];
leftView.backgroundColor = [UIColor greenColor];
[self.view addSubview:leftView];
UIView *rightView = [[UIView alloc] initWithFrame:CGRectMake(667-50, 375-50, -275, -275)];
rightView.backgroundColor = [UIColor blueColor];
[self.view addSubview:rightView];
And we got:
We will find that rightView's frame is automatically changed to positive value, which is (342, 50, 275, 275), but its bounts.origin = (-275,-275).
And we add subviews:
UIView *leftSubview = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
leftSubview.backgroundColor = [UIColor grayColor];
[leftView addSubview:leftSubview];
UIView *rightSubview= [[UIView alloc] initWithFrame:CGRectMake(0, 0, 30, 30)];
rightSubview.backgroundColor = [UIColor grayColor];
[rightView addSubview:rightSubview];
So the bounds makes rightView's subview follows the origin which we init rightView.
If we change the bounds of rightView equals to leftView:
rightView.bounds = leftView.bounds;
Then the two subViews location is the same, we lost the information that rightView's width and height are negative.
And we change the bounds of leftView instead of rightView:
CGRect bounds = leftView.bounds;
bounds.origin = CGPointMake(50, 50);
leftView.bounds = bounds;
We can see, its subview's frame.origin is offset by bounds.origin(using minus, not plus).
To conclude:
view.bounds determines all its subview's location(offset by bounds.origin), while bounds will not affect its own location in its superview.
If you init a view with negative width and height, it will automatically changed to positive(which won't change the location), but its bounds.origin indicates the point that you start to "draw" the view.
A UIScrollView's bounds.origin will not be (0, 0) when its contentOffset is not (0, 0).
The bounds.origin will be negative if you initialize a view with negative width/height.
For example, if you did
UIView* v = [[UIView alloc] initWithFrame:CGRectMake(5, 5, -10, -20)];
the frame would be:
origin = {
x = -5,
y = -15
},
size = {
width = 10,
height = 20
}
bounds:
origin = {
x = -10,
y = -20
},
size = {
width = 10,
height = 20
}
center:
x = 0,
y = -5
try it for yourself!
(edited again because I can’t delete my original answer after it was accepted—credit for this goes to ian, who posted a more thorough answer below:)
In most cases this won’t happen. If you initialize your view with a negative width and/or height, you’ll get an origin with a negative X of the width and/or negative Y of the height.

Resources