Simple way to redraw view programatically when device orientation changes - ios

I'm new to iOS development and for my assignment, I'm tasked changing updating the ViewController programmatically when the device's orientation changes. I've found a snippet of an answer here, but it doesn't get the job done.
I tried adding this to the viewWillLayoutSubviews of my View Controller, but all I get is an unused variable warning.
CGRect rotatedFrame = [self.view convertRect:self.view.frame fromView:self.view.superview];
viewWillLayoutSubviews and rotation
As a "hint", I've been told it's simple to implement in viewWillLayoutSubviews. Going through and changing all the CGRects in my VC doesn't sound like a couple of lines of code. There's got to be a simpler, more efficient way to do this, but I've only found snippets of solutions digging around on this site. Thanks for reading.

The line of code you are using is assigning a CGRect to the rotatedFrame variable, it's not updating anything on your view controller.
There's many ways to approach this but it depends on what is contained in your view and how it's been configured. Things like Auto Layout for example could let you configure almost everything in Interface Builder and let you avoid doing most things in code.
You've been tasked to do this programatically and since we know that viewWillLayoutSubviews is called every time the device is rotated that's a good place to start. Here's a lazy way I've gone about rotating a video to fit a new orientation using a transformation:
//Vertical
CGSize size = self.view.frame.size;
someView.transform = CGAffineTransformMakeRotation((M_PI * (0) / 180.0))
someView.frame = CGRectMake(0, 0, MIN(size.width, size.height), MAX(size.width, size.height));
//Horizontal
CGSize size = [UIScreen mainScreen].bounds.size;
int directionModifier = ([UIDevice currentDevice].orientation == UIInterfaceOrientationLandscapeLeft) ? -1 : 1;
someView.bounds = CGRectMake(0, 0, MAX(size.width, size.height), MIN(size.width, size.height));
someView.transform = CGAffineTransformMakeRotation((M_PI * (90) / 180.0) *directionModifier);
someView.transform = CGAffineTransformTranslate(someView.transform,0,0);
How many subviews are in your view? Are they grouped? If you're using auto-resizing masks you might get away with only adjusting the frames of one or two views. If your root view has a number of subviews you can loop through views that need similar adjustments to avoid having to write excess code. It really depends on how everything has been set up.

I figured out how to determine the viewWidth and viewHeight and set those as CGFloats. I then added an if-else statement which figures out if the display is in portrait or landscape and sets the problematic calculateButton accordingly.
Apologies for lengthy code, but I've found in searching this site I find "snippets" of answers, but being new to iOS it's difficult to figure out what goes where. Hopefully, this helps someone later. (and hopefully it's correct)
- (void) viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
CGFloat viewWidth = self.view.bounds.size.width;
CGFloat viewHeight = self.view.bounds.size.height;
CGFloat padding = 20;
CGFloat itemWidth = viewWidth - padding - padding;
CGFloat itemHeight = 44;
// Bunch of setup code for layout items
// HOMEWORK: created if-else statement to deal w/ portrait vs. landscape placement of calculateButton.
if (viewWidth > viewHeight) {
// portrait
CGFloat bottomOfLabel = viewHeight;
self.calculateButton.frame = CGRectMake(padding, bottomOfLabel - itemHeight, itemWidth, itemHeight);
} else {
// landscape
CGFloat bottomOfLabel = CGRectGetMaxY(self.resultLabel.frame);
self.calculateButton.frame = CGRectMake(padding, bottomOfLabel + padding, itemWidth, itemHeight);
}
}

Related

Find bounds of UIViewController's view not obscured by UINavigationBar, UITabBar, (etc)

I can configure my UIViewController's edgesForExtendedLayout so that it will extend underneath content such as the navigation bar or tab bar. If I do this, is there some way to determine the frame that is not obscured?
As a possible alternative, is there a way for a UIViewController to determine the default contentInset to apply to a UIScrollView it contains?
Use case
I have zoomable UIScrollView containing an image.
When it is fully zoomed out I want to adjust the content inset too allow the content to stay centred (details here). However, my modified insets don't take in to account the insets that the UIViewController applies automatically so that its content isn't obscured by navigation bars, etc.
I also need to compute the minimum zoom for the content – that at which the whole image will be visible and not obscured. To compute this, I need to know the size of the unobscured part of the content view.
You need this
-(CGRect) unobscuredBounds
{
CGRect bounds = [self.view bounds];
return UIEdgeInsetsInsetRect(bounds, [self defaultContentInsets]);
}
-(UIEdgeInsets) defaultContentInsets
{
const CGFloat topOverlay = self.topLayoutGuide.length;
const CGFloat bottomOverlay = self.bottomLayoutGuide.length;
return UIEdgeInsetsMake(topOverlay, 0, bottomOverlay, 0);
}
You could put this in a category for easy reusability.
These methods correctly handle the changes that occur when the view resizes after a rotation – the change to the UINavigationBar size is correctly handled.
Centring Content
To use this to centre content by adjusting insets, you'd do something like this:
-(void) scrollViewDidZoom:(UIScrollView *)scrollView
{
[self centerContent];
}
- (void)centerContent
{
const CGSize contentSize = self.scrollView.contentSize;
const CGSize unobscuredBounds = [self unobscuredBounds].size;
const CGFloat left = MAX(0, (unobscuredBounds.width - contentSize.width)) * 0.5f;
const CGFloat top = MAX(0, (unobscuredBounds.height - contentSize.height)) * 0.5f;
self.scrollView.contentInset = UIEdgeInsetsMake(top, left, top, left);
}
Your content insets will now reflect the default insets that they need (to avoid being covered up) and will also have the insets they need to be nicely centred.
Handling Rotation & Zoom
You probably also want to perform centring when animating between landscape and portrait. At the same time, you might want to adjust your minimum zoom scale so that your content will always fit. Try out something like this:
-(void) willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[self centerContent];
const bool zoomIsAtMinimum = self.scrollView.zoomScale == self.scrollView.minimumZoomScale;
self.scrollView.minimumZoomScale = [self currentMinimumScale];
if(zoomIsAtMinimum)
{
self.scrollView.zoomScale = self.scrollView.minimumZoomScale;
}
}
-(CGFloat) currentMinimumScale
{
const CGFloat currentScale = self.scrollView.zoomScale;
const CGSize scaledContentSize = self.scrollView.contentSize;
const CGSize scrollViewSize = [self unobscuredBounds].size;
CGFloat scaleToFitWidth = currentScale * scrollViewSize.width / scaledContentSize.width;
CGFloat scaleToFitHeight = currentScale * scrollViewSize.height / scaledContentSize.height;
return MIN(scaleToFitWidth, scaleToFitHeight);
}
The willAnimateRotationToInterfaceOrientation:… method is called within the view animation block, so the changes that it applies will lead to nice smooth animated changes as you switch from landscape to portrait.

Incorrect row height returned when iOS device rotated

I'm having an issues with displaying UITableViewCells with the correct height in a UITableView once the device is rotated.
When the table view is initially loaded the rows all have the correct size as you can see on the image below:
However, when I rotate the device to landscape mode I get shorter rows than I would like and consequently, parts of the view's background are visible, see here:
When turning the phone back to portrait mode again, the row sizes are larger then when the view was loaded for the first time and as a result, the bottom cell seems smaller, and the text does not look centred in the cell, see here:
This is the method and variables I'm using for row height calculation:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
CGFloat screenHeight = [UIScreen mainScreen].bounds.size.height;
CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width;
CGFloat statusBarHeight = [UIApplication sharedApplication].statusBarFrame.size.height;
CGFloat statusBarWidth = [UIApplication sharedApplication].statusBarFrame.size.width;
CGFloat navigationBarHeight = self.navigationController.navigationBar.frame.size.height;
CGFloat rowHeight;
NSUInteger numberOfRows = self.orderedCategories.count;
if (orientation == UIDeviceOrientationPortrait) {
rowHeight = ((screenHeight - navigationBarHeight - statusBarHeight) / numberOfRows);
NSLog(#"portrait: %f", rowHeight);
} else {
rowHeight = ((screenWidth - navigationBarHeight - statusBarWidth) / numberOfRows);
NSLog(#"landscape: %f", rowHeight);
}
return rowHeight;
}
I don't have a definite answer, but here are a few things to consider:
1) why are you calling [self.tableview setNeedsDisplay] inside tableView:heightForRowAtIndexPath? If you need to reset the display here, there's probably something wrong with your overall logic.
2) what about moving the calculation code out to the didRotateFromInterfaceOrientation: method? Then call reloadData when you're done. Store the height in an instance variable, and just return that in tableView:heightForRowAtIndexPath:.
2) does this code get run after the rotation. Try logging out the different constants and double check the math by hand. That may give you a better idea of what's going on. There may be a timing issue--for example, you may be getting the status bar height before the rotation occurs, instead of after it occurs.

Unwanted image animation when resizing UITableView

For an iPad project I implemented an NSBrowser-like interface wich supports a dynamic number of columns. Each column is represented by an UITableView.
When adding or removing columns, I'm using UIView's animateWithDuration:animations: to change the width of the UITableView.
The problem I'm running into:
The system adds an unwanted frame size animation for the imageView of each table view cell, which resizes to imageView from very large to it's initial size. This looks really awkward, so I'm looking for ways to get rid off it - while keeping the animated frame size change for the enclosing tableViews.
Any ideas what I might be doing wrong?
I posted a sample project demonstration the issue here:
https://github.com/iljaiwas/TableViewFrameTest
Here is where I setup the cell:
https://github.com/iljaiwas/TableViewFrameTest/blob/master/TableViewFrameTest/TestTableViewController.m#L61
Here is where I trigger the animation:
https://github.com/iljaiwas/TableViewFrameTest/blob/master/TableViewFrameTest/TestViewController.m#L46
I was having the same issue and found this link (http://www.objc.io/issue-5/iOS7-hidden-gems-and-workarounds.html) which has a section on Blocking Animations.
To get your example working add the following at the top of TestTableViewController.m after the import statement:
#interface MyTableViewCell : UITableViewCell
#end
#implementation MyTableViewCell
- (void) layoutSubviews {
[UIView performWithoutAnimation:^{
[super layoutSubviews];
}];}
#end
Then change the following line in viewDidLoad to use MyTableViewCell:
[self.tableView registerClass:[MyTableViewCell class] forCellReuseIdentifier:#"Cell"];
Now run the example and you will no longer have your unwanted image animation.
this hepled to me:
set UIImageView ** (in TableView cell) **contentMode to aspect Fit.
do not know why, but works for me.
I was having a similar issue with the (pretty large) image shrinking down from its original size when the editing animation fired to show the delete button. The picture would fly across my screen as it shrank. Pretty crazy. Anyway, I fixed it by using this category on UIImage to resize the image before putting it in the UIImageView:
- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)scaledSize {
UIGraphicsBeginImageContext(scaledSize);
[image drawInRect:CGRectMake(0, 0, scaledSize.width, scaledSize.height)];
UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return scaledImage;
}
Edit: here's another category that does a much better job of scaling down the image, depending on how good you need it to look. I originally found these on another SO question a while back but can't seem to find it now to link to, but I used them as a starting point and this one was especially helpful:
- (UIImage *)imageScaledToFitRect:(CGRect)rect {
// compute scale factor for imageView
CGFloat widthScaleFactor = CGRectGetWidth(rect) / self.size.width;
CGFloat heightScaleFactor = CGRectGetHeight(rect) / self.size.height;
NSLog(#"Rect width: %f, Image width: %f, WidthFactor: %f", rect.size.width, self.size.width, widthScaleFactor);
NSLog(#"Rect height: %f, Image height: %f, HeightFactor: %f", rect.size.height, self.size.height, heightScaleFactor);
CGFloat imageViewXOrigin = 0;
CGFloat imageViewYOrigin = 0;
CGFloat imageViewWidth = 0;
CGFloat imageViewHeight = 0;
// if image is narrow and tall, scale to width and align vertically to the top
if (widthScaleFactor > heightScaleFactor) {
imageViewWidth = self.size.width * widthScaleFactor;
imageViewHeight = self.size.height * widthScaleFactor;
}
// else if image is wide and short, scale to height and align horizontally centered
else {
imageViewWidth = self.size.width * heightScaleFactor;
imageViewHeight = self.size.height * heightScaleFactor;
imageViewXOrigin = - (imageViewWidth - CGRectGetWidth(rect))/2;
}
return [self imageScaledToRect:CGRectMake(imageViewXOrigin, imageViewYOrigin, imageViewWidth, imageViewHeight)];
}
Hope this can help someone else.

Auto-resize UIView to fit iPhone 5 screen while maintaining proportion

My StoryBoard is configured for iPhone4 resolution, and when running on iPhone 5 I'd like a UIView to get bigger (both height&width).
The problem right now is that the view is only getting higher and not wider. What should be the auto-resize configuration in order to achieve this?
You will probably need to use a subclass of UIView with the setFrame: method overridden to catch frame changes.
- (void)setFrame:(CGRect)frame
{
frame.size.width = frame.size.height; // Make the *width* always equal to the *height*. Logic could go here, etc.
[super setFrame:frame]
}
Reference the height of the screen and use some padding values to set your view frame. Below is code to set the frame of a UIView called yourShapeView:
// Get the frame of the screen
CGRect screenFrame = [[UIScreen mainScreen] bounds];
// Set padding from the top and bottom of the shape
CGFloat verticalPadding = 40.0f;
CGFloat horizontalPadding = 20.0f;
// Get the height/width values
CGFloat height = screenFrame.size.height - (verticalPadding * 2);
CGFloat width = screenFrame.size.width - (horizontalPadding * 2);
// Set the size of your shape based on the height and padding
yourShapeView.frame = CGRectMake(horizontalPadding, verticalPadding, width, height);

On iOS, what is the best way to set up a subview if we really would like to do initWithCenterAndSize?

For example, if we are to draw a 100 x 100 pixel circle on the main view which covers up the whole screen on the iPad, then instead of using initWithFrame like following 2 steps in viewDidLoad:
UINodeView *nodeView = [[UINodeView alloc] initWithFrame:
CGRectMake(x, y, NSNodeWidth, NSNodeHeight)];
nodeView.center = CGPointMake(x, y);
because x and y is more elegantly as self.view.bounds.size.width / 2 to horizontally center the circle, instead of self.view.bounds.size.width / 2 - NSNodeWidth / 2. Is init by a frame first, and then reset the center a good way, or is there a better way, if there is a initWithCenterAndSize?
That's a fine way of doing it :)
I would have gone for generating the positioned frame first to avoid the extra method call but that's just a matter of personal preference :)
If you're using this alot in your app you could make a category on UIView that implements this (warning, untested code :)
- (id)initWithCenter:(CGPoint)point size:(CGSize)size {
CGRect frame = CGRectMake(point.x-size.width/2,
point.y-size.height/2,
size.width,
size.height);
return [self initWithFrame:frame];
}
I usually do this:
UINodeView *nodeView = [[UINodeView alloc] initWithFrame:
CGRectMake(0, 0, NSNodeWidth, NSNodeHeight)];
nodeView.center = CGPointMake(x, y);
It looks nice and clear.

Resources