So after compiling an app on XCode 6, I noticed a strange bug that happens only when running on iOS 8:
The UITableView takes the wrong inner dimensions after updating its frame.
Now I'll try to explain the exact situation:
We have a UITableView rotated on its side, which basically makes a horizontal UITableView. It happens through tableView.transform = CGAffineTransformMakeRotation(-M_PI / 2);.
Now after setting the transform, and then settings its frame - everything is fine.
But of course the system in most cases sends the parent another frame change because it needs to set the parent to the real sizes and not the XIB sizes or any initialization size. In that moment - when I relayout the subviews, including the table view - everything goes wrong.
Actually the frame of the table view is simply set to the bounds of the containing view, but then the inner scrollview (In iOS 8 the UITableView has another UIScrollView inside it, called UITableViewWrapperView. As UITableView is a UIScrollView by itself, I can't figure out why they needed another one...) takes a "height" which equals the parent width. And "height" is actually the width property, only rotated.
Now we can easily estimate the they have a bug with relating the width of the inner UIScrollView to the actual width of the parent UITableView, which could possibly be by reading the .frame.size.width instead of the .bounds.size.width.
But the strange thing is that when investigating the frame of the subviews of the UITableView- it seems that they are all good! So it must be a rendering problem somewhere.
So we are left with a horizontal table which has a blank gap on top, because the "height" of the cells is 320 instead of 568, while the "width" of the cells is fine, set to 320.
I'll be very happy to hear from other people experiencing this problem (Or from Apple), but I have finally found a solution and posting it here with the question, for future reference for me and for others.
So the change that made it behave, was instead of doing this:
- (void)layoutSubviews
{
tableView.frame = self.bounds;
}
I have reset the transform, set the frame to the bounds which the UITableView would expect locally after the transform, and then set the transform and set the correct frame. This is a bit confusing, but here it goes:
- (void)layoutSubviews
{
if (UIDevice.currentDevice.systemVersion.floatValue >= 8.f)
{
// iOS 8 layout bug! Table's "height" taken from "width" after changing frame. But then if we cancel transform, set width/height to the final width/height, and rotate it and set to the virtual width/height - it works!
CGRect rotatedFrame = self.bounds,
unrotatedFrame = rotatedFrame;
unrotatedFrame.size.width = rotatedFrame.size.height;
unrotatedFrame.size.height = rotatedFrame.size.width;
tableView.transform = CGAffineTransformIdentity;
tableView.frame = unrotatedFrame;
tableView.transform = CGAffineTransformMakeRotation(-M_PI / 2);
tableView.frame = rotatedFrame;
}
else
{
tableView.frame = self.bounds;
}
}
This appears to be a new problem with iOS8. When you want to rotate an object it no longer appears to rotate around the upper left corner of the object's frame.
Apple docs for iOS8 state that "an object is rotated about it's center point". So when a vertical UITableView is rotated 90 degrees, it may disappear from view because the center point may be off the visible area. In order to make the table appear as if it was rotated about the upper left corner of the table, you must now also translate the frame by an amount equal to the difference between the frame width and frame height.
It's important to note you need to concatenate the transforms in order to get the desired result, like the following:
First create a 90 degree rotation transform:
CGAffineTransform xform_rotate = CGAffineTransformMakeRotation(-M_PI * 0.5);
Then create a translation amount variable equal to the difference between table width and height:
float translateAmount = (camThumbsTableView.frame.size.height/2)-(camThumbsTableView.frame.size.width/2);
Then concatenate the original rotation transform with the translation:
CGAffineTransform xform_total = CGAffineTransformTranslate(xform_rotate, translateAmount, translateAmount);
When done, you can now transform your tableView as follows:
self.camThumbsTableView.transform = xform_total;
This will have the effect of both rotating and translating your tableView such that it now appears to have been rotated around the upper left corner of the tableView instead of about the center point.
Related
How do you handle centering and scrolling content in a UIScrollView when the dimensions are both larger and smaller than the containing scroll view?
I have been trying to set values in layoutSubviews to handle the initial centering of the content, while still allowing for scrolling.
If the content is smaller in both dimensions, I can just set the frame and the image is properly centered for all rotations and orientations. Setting the contentInset will also work. contentOffset does not seem to work.
If the content is larger in both dimensions, I can set contentOffset for the initial display, and not modify it again to support scrolling.
What do I do if I have an image with one dimension larger, and the other smaller, than the scroll view?
contentOffset uses a CGPoint, and contentInset uses UIEdgeInsets (top, left, bottom, right). I have tried mixing positive and negative, since one dimension needs to be moved in and the other out, but haven't gotten anything to work.
My next thought is to resize the scroll view (and modify constraints I suppose) so that the content is never smaller than the container and use contentOffset.
I would really like to have a single approach that will work regardless of larger or smaller dimensions.
What is the best solution (a solution) to this problem?
After stepping back from this, getting a better understanding of UIScrollView, and rethinking, I have solved my problem.
For starters, layoutSubviews is the wrong way to go, at least for what I need.
Trying to resize the UIScrollView and update constraints seemed more trouble than it was worth.
For whatever reason, it didn't initially occur to me than I could use both contentOffset and contentInset at the same time (thought it was either/or for some reason), but that was my exact solution.
CGRect rectContent = CGRectMake(0, 0, image.size.width, image.size.height);
UIImageView *imageView = [[UIImageView alloc] initWithFrame:rectContent];
self.scrollView.contentSize = rectContent.size;
CGFloat fOffsetWidth = (rectContent.size.width < self.scrollView.bounds.size.width) ? (self.scrollView.bounds.size.width - rectContent.size.width)/2 : 0;
CGFloat fOffsetHeight = (rectContent.size.height < self.scrollView.bounds.size.height) ? (self.scrollView.bounds.size.height - rectContent.size.height)/2 : 0;
self.scrollView.contentInset = UIEdgeInsetsMake(fOffsetHeight, fOffsetWidth, fOffsetHeight, fOffsetWidth);
self.scrollView.contentOffset = CGPointMake((rectContent.size.width - self.scrollView.bounds.size.width)/2, (rectContent.size.height - self.scrollView.bounds.size.height)/2);
imageView.image = image;
[self.scrollView addSubview:imageView];
All image dimension possibilities (larger/smaller, one/both) are centered in the scroll view, and a larger image dimension is scrollable while a smaller dimension remains centered.
Perfect!
I am implementing an app with a trackbar which itself is a view and it needs to display the minimum and maximum values (of the variable which is associated with the bar) so I have added two labels to the top left and top right of it. Think of something like this without an enabled slider:
I would like to be able to shrink or magnify this view with pinch gesture and the below code does work fine :
-(void) handlePinch:(UIPinchGestureRecognizer *)gr
{
//Shrinking
if(gr.scale < 1)
{
//Get screen width
CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
//If the view's would be size is smaller than half the screen's size then don't do anything
//Otherwise shrink the view
if(self.frame.size.width * gr.scale >= screenWidth / 2)
{
//Only scale on x axis, y axes stays the same
self.transform = CGAffineTransformScale(self.transform, gr.scale, 1);
}
}
//Magnifying
else if (gr.scale > 1)
{
//Only scale on x axis, y axes stays the same
self.transform = CGAffineTransformScale(self.transform, gr.scale, 1);
}
//Set scale amount back to 1
gr.scale = 1;
}
The problem is when the view is shrank the labels on top left and right are also shrank and their font size gets smaller. Since I scale the view only horizontally this looks bizarre. I want to set the labels' size constant and shrink everything else inside the view.
I have tried to assign a new frame rectangle with the original labels' sizes after shrinking but it didn't work.Do you have any tips?
edit : Setting minimumFontSize property did not work either (I don't try minimumScaleFactor because I'm still using ios 5 sdk)
I think this behavior is actually controlled by the view you have your label on - it's not controlled by the label itself. There is a autoresizesSubviews property of UIView that defaults to YES, and that makes anything inside a view shrink when the UIView shrinks. Since your pinch gesture is actually attached to the view behind the label and not the label itself, your pinch is shrinking the view, then the view shrinks the label. Try setting autoresizesSubviews = NO, and let me know if that works for you.
I found the solution in separating my trackbar from the view that shall support pinch gesture. It was bad design to include trackbar in it beforehand, check your ui design before putting effort on more complicated stuff.
I'm building an iPad app with views that are split horizontally and animate in from the top and bottom (think of jaws sliding closed and open to appear and disappear respectively).
My problem is the layout of the custom jaws subview is broken only when the view loads in a landscape orientation. (The jaws-view container loads at the proper size, but the subsequent subviews for the top and bottom half are too tall, and going off the screen. They are the correct width though.)
I can start in portrait and then rotate and everything is arranged correctly.
I've tried setting the frame of the new view to the bounds of the original in a bunch of places (as suggested by many answers that didn't work for me, links upon request) but either haven't found the right spot, or need something more.
Do I need to do anything special to get the size to propagate? Is there a point before which I should not do animation? (I'm trying to move the top and bottom in my new view controller's viewDidLoad.)
The solution to this required 2 parts.
The first was described in this answer:
https://stackoverflow.com/a/8574519/1143123
which describes using viewWillAppear method instead of viewDidLoad (called earlier and "incorrect" values for bounds). This solved the problem of the view being layed out properly when loading that view in landscape (and propagated to subsequent rotations).
The second part was that the view could still get messed up if I started animating it and then did a rotation in the meantime. I changed my animation class to only move the center coordinate (as opposed to sliding the frame) which would have been better in the first place, but that didn't solve it. In the end I hardcoded the following in the ViewController for the class exhibiting these issues:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
[super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
// The container view should match the size of the current view
gameView.frame = self.view.bounds;
CGFloat width = self.view.bounds.size.width;
if(roundInProgress) {
gameView.jawsTop.frame = CGRectMake(0, 0, width, self.view.bounds.size.height/2);
gameView.jawsBottom.frame = CGRectMake(0, self.view.bounds.size.height/2, width, self.view.bounds.size.height/2);
} else {
// If round not in progress, game cards should be offscreen
CGFloat height = self.view.bounds.size.height/2;
gameView.jawsTop.frame = CGRectMake(0, -height, width, height);
gameView.jawsBottom.frame = CGRectMake(0, self.view.bounds.size.height, width, height);
}
}
Without seeing your code my guess is that it has to do with the "Autoresize Subviews" property of your parent view and/or the autosizing set-up for your subviews. Try changing that property in Interface Builder to see if that fixes your issue.
Am fooling around with this question a couple of days now but no progress. What i want to do is quite simple i think:
I have an image of 320x60 which i use in the plain TableView which works oke as those cells take up the entire width (320) of the screen. The grouped cells in a TableView are 300 wide and have insets/margins left of 10 on the left and the right.
Can i somehow remove those insets/margins and let the grouped cell be 320 wide? I tried setting the content inset left to -10. That does "remove" the left margin but then it's still only 300 wide. Also tried editing the XML of the storyboard (I'm working with iOS 5 - Storyboards) but no joy.
This similar question here got answered as no it's not possible, hopfully something changed in 2+ years!:
Adjust cell width in grouped UITableView
PS i want to alter the width as the background images contain nice shadows, I've read that exesive use of shadows could mean performance issues. Also the shadow's are 5px extra around the border so that would mean -10px wide if I use the standard width.
Help much appreciated!
An untidy solution is to make the table view 340 pixels wide, and 10 pixels off the left edge of the screen.
A solution that involves changing properties of private classes is to make a UITableViewCell subclass, and override its layoutSubviews method. When I log the subviews, I find these:
"<UIGroupTableViewCellBackground: 0x95246b0; frame = (9 0; 302 45); autoresize = W; layer = <CALayer: 0x95226b0>>",
"<UITableViewCellContentView: 0x92332d0; frame = (10 0; 300 43); layer = <CALayer: 0x9233310>>",
"<UIView: 0x95248c0; frame = (10 0; 300 1); layer = <CALayer: 0x951f140>>"
What happens if we take those subviews and fill the entire bounds available?
- (void)layoutSubviews;
{
// By default the cell bounds fill the table width, but its subviews (containing the opaque background and cell borders) are drawn with padding.
CGRect bounds = [self bounds];
// Make sure any standard layout happens.
[super layoutSubviews];
// Debugging output.
NSLog(#"Subviews = %#", [self subviews]);
for (UIView *subview in [self subviews])
{
// Override the subview to make it fill the available width.
CGRect frame = [subview frame];
frame.origin.x = bounds.origin.x;
frame.size.width = bounds.size.width;
[subview setFrame:frame];
}
}
At this particular moment, on the iOS 5.1 simulator, this works. Now, some future version of iOS may restructure these classes, causing this method to catastrophically mangle the table layout. Your app could be rejected for changing the properties of UITableViewCellContentView... even though you're only modifying its frame. So, how much do you need to have your cells fill the table width?
You can the UITableView's Leading and Trailing Space constraints in the Size Inspector which is accessible via the Storyboard. I'm not sure when this was added, but setting the Leading Space Constraint to -10 and the Trailing Space Constraint to 10 will make the cells full width.
I have a UIScrollView that contains several dynamically resizing subviews. I can resize and layout the subviews just fine, but when I set the content size of the scroll view itself, the bottom subviews are clipped. Is there some reason why a scroll view's content size height should be larger than the sum of the heights of the views it contains?
Here's my situation in more detail:
I have a superview containing a UIScrollView containing several subviews. In the superview's layoutSubviews method, I calculated the needed size of each subview, then set the frames so the subviews are tiled vertically down the screen with a bit of space between them. When done, I set the height of the UIScrollView's content size to be the end of the last subview (origin.y + size.height). In theory, this means the bottom of the scroll view's content area should exactly line up with the bottom of the last subview.
But it doesn't. Instead, a nice chunk of the last subview is clipped. It's still there - if I scroll down I can see the remaining portion during the "bounce". The problem is even worse in landscape mode - a much larger portion of the bottom subview simply isn't visible.
The subviews are all being arranged and positioned properly. The problem is that the UIScrollView's contentSize seems to need to be significantly larger than the sum of the heights of the subviews (plus the space between them). This doesn't make any sense to me. Furthermore, the amount the size is "off" varies - I reuse this view several times with different subviews, and they're all off by a different amount. Therefore, simply adding a constant to the content view height won't help.
What is causing the content size (or my height calculations) to not function correctly?
Code:
- (void)layoutSubviews
{
[super layoutSubviews];
CGFloat width = self.bounds.size.width - [self subviewLeftMargin] - [self subviewRightMargin]; // All subviews have same width as parent view
CGFloat x = [self subviewLeftMargin]; // All subviews should start at the far left of the view
CGFloat y = [self spaceBetweenSubviews]; // Running tally of the y coordinate for the next view
/* Adjust the subviews */
for(UIView *view in self.theVariousSubviews) {
/* Resize the view with the desired width, then let it size its height as needed */
view.frame = CGRectMake(view.frame.origin.x, view.frame.origin.y, width, view.frame.size.height);
CGSize newSize = [view sizeThatFits:view.frame.size];
/* Set the origin */
//The subviews are positioned correctly, so this doesn't seem to be a problem
view.frame = CGRectMake(x, y, newSize.width, newSize.height);
/* Have the view refresh its own layout */
[view setNeedsLayout];
/* Update the y value for the next subview */
y += newSize.height + [self spaceBetweenSubviews];
}
/* Resize the scroll view to ensure it fits all of the content */
CGFloat scrollViewHeight = y;
self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width, scrollViewHeight);
//Content size is set to the same total height of all subviews and spacing, yet it is too small. Why?
}
hi it seems to me that your calculation and resizing timing is wrong.
Without the missing code for the layout change I could not fully understand the problem.
What strikes me is that you are assigning view.frame twice and between the new calculation you intercept the process with sublayouting which might change some of the values your calculation is depending on.
I could only advice you to separate the calculation from layouting and not invoke methods while you are calculating. To bring light into it you should either drop a sample app with the missing calculation or for yourself add some NSLog statement showing you the frame origin size of any subview and the contentOffset for the scrollview.
On my experiences the scrollview is working properly in general so I would expect a bug within your code.