I'm very new to landscape adaptations. My UIScrollView with an embedded UIImageView works perfect in portrait, but when I switch to landscape it's incredibly messed up.
I tried the following:
- (void)viewDidLayoutSubviews {
self.scrollView.frame = self.view.frame;
}
But it's to no avail. When I rotate the view it still throws the image into the bottom left corner.
How do I layout a UIScrollView so when I shift to landscape, it stays centered? (Pretty much identical behavior to Photos.app is what I'm striving for.)
In my scrollViewDidZoom: I have the following as well, if relevant (it keeps it centered as the user scrolls):
CGRect newImageViewFrame = self.imageView.frame;
// Center horizontally
if (newImageViewFrame.size.width < CGRectGetWidth(scrollView.bounds)) {
newImageViewFrame.origin.x = (CGRectGetWidth(scrollView.bounds) - CGRectGetWidth(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.x = 0;
}
// Center vertically
if (newImageViewFrame.size.height < CGRectGetHeight(scrollView.bounds)) {
newImageViewFrame.origin.y = (CGRectGetHeight(scrollView.bounds) - CGRectGetHeight(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.y = 0;
}
self.imageView.frame = newImageViewFrame;
Related
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.
I'm having a lot of trouble figuring out how best to reposition my UIScrollView's image view (I have a gallery kind of app going right now, similar to Photos.app, specifically when you're viewing a single image) when the orientation switches from portrait to landscape or vice-versa.
I know my best bet is to manipulate the contentOffset property, but I'm not sure what it should be changed to.
I've played around a lot, and it seems like for whatever reason 128 works really well. In my viewWillLayoutSubviews method for my view controller I have:
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.x >= 128) {
newContentOffset.x -= 128.0;
}
else {
newContentOffset.x = 0.0;
}
newContentOffset.y += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
else {
CGPoint newContentOffset = self.scrollView.contentOffset;
if (newContentOffset.y >= 128) {
newContentOffset.y -= 128.0;
}
else {
newContentOffset.y = 0.0;
}
newContentOffset.x += 128.0;
self.scrollView.contentOffset = newContentOffset;
}
And it works pretty well. I hate how it's using a magic number though, and I have no idea where this would come from.
Also, whenever I zoom the image I have it set to stay centred (just like Photos.app does):
- (void)centerScrollViewContent {
// Keep image view centered as user zooms
CGRect newImageViewFrame = self.imageView.frame;
// Center horizontally
if (newImageViewFrame.size.width < CGRectGetWidth(self.scrollView.bounds)) {
newImageViewFrame.origin.x = (CGRectGetWidth(self.scrollView.bounds) - CGRectGetWidth(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.x = 0;
}
// Center vertically
if (newImageViewFrame.size.height < CGRectGetHeight(self.scrollView.bounds)) {
newImageViewFrame.origin.y = (CGRectGetHeight(self.scrollView.bounds) - CGRectGetHeight(self.imageView.frame)) / 2;
}
else {
newImageViewFrame.origin.y = 0;
}
self.imageView.frame = newImageViewFrame;
}
So I need it to keep it positioned properly so it doesn't show black borders around the image when repositioned. (That's what the checks in the first block of code are for.)
Basically, I'm curious how to implement functionality like in Photos.app, where on rotate the scrollview intelligently repositions the content so that the middle of the visible content before the rotation is the same post-rotation, so it feels continuous.
You should change the UIScrollView's contentOffset property whenever the scrollView is layouting its subviews after its bounds value has been changed. Then when the interface orientation will be changed, UIScrollView's bounds will be changed accordingly updating the contentOffset.
To make things "right" you should subclass UIScrollView and make all the adjustments there. This will also allow you to easily reuse your "special" scrollView.
The contentOffset calculation function should be placed inside UIScrollView's layoutSubviews method. The problem is that this method is called not only when the bounds value is changed but also when srollView is zoomed or scrolled. So the bounds value should be tracked to hint if the layoutSubviews method is called due to a change in bounds as a consequence of the orientation change, or due to a pan or pinch gesture.
So the first part of the UIScrollView subclass should look like this:
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
// Set the prevBoundsSize to the initial bounds, so the first time
// layoutSubviews is called we won't do any contentOffset adjustments
self.prevBoundsSize = self.bounds.size;
}
return self;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (!CGSizeEqualToSize(self.prevBoundsSize, self.bounds.size)) {
[self _adjustContentOffset];
self.prevBoundsSize = self.bounds.size;
}
[self _centerScrollViewContent];
}
Here, the layoutSubviews method is called every time the UIScrollView is panned, zoomed or its bounds are changed. The _centerScrollViewContent method is responsible for centering the zoomed view when its size becomes smaller than the size of the scrollView's bounds. And, it is called every time user pans or zooms the scrollView, or rotates the device. Its implementation is very similar to the implementation you provided in your question. The difference is that this method is written in the context of UIScrollView class and therefore instead of using self.imageView property to reference the zoomed view, which may not be available in the context of UIScrollView class, the viewForZoomingInScrollView: delegate method is used.
- (void)_centerScrollViewContent {
if ([self.delegate respondsToSelector:#selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
CGRect frame = zoomView.frame;
if (self.contentSize.width < self.bounds.size.width) {
frame.origin.x = roundf((self.bounds.size.width - self.contentSize.width) / 2);
} else {
frame.origin.x = 0;
}
if (self.contentSize.height < self.bounds.size.height) {
frame.origin.y = roundf((self.bounds.size.height - self.contentSize.height) / 2);
} else {
frame.origin.y = 0;
}
zoomView.frame = frame;
}
}
But the more important thing here is the _adjustContentOffset method. This method is responsible for adjusting the contentOffset. Such that when UIScrollView's bounds value is changed the center point before the change will remain in center. And because of the condition statement, it is called only when UIScrollView's bounds is changed (e.g.: orientation change).
- (void)_adjustContentOffset {
if ([self.delegate respondsToSelector:#selector(viewForZoomingInScrollView:)]) {
UIView *zoomView = [self.delegate viewForZoomingInScrollView:self];
// Using contentOffset and bounds values before the bounds were changed (e.g.: interface orientation change),
// find the visible center point in the unscaled coordinate space of the zooming view.
CGPoint prevCenterPoint = (CGPoint){
.x = (self.prevContentOffset.x + roundf(self.prevBoundsSize.width / 2) - zoomView.frame.origin.x) / self.zoomScale,
.y = (self.prevContentOffset.y + roundf(self.prevBoundsSize.height / 2) - zoomView.frame.origin.y) / self.zoomScale,
};
// Here you can change zoomScale if required
// [self _changeZoomScaleIfNeeded];
// Calculate new contentOffset using the previously calculated center point and the new contentOffset and bounds values.
CGPoint contentOffset = CGPointMake(0.0, 0.0);
CGRect frame = zoomView.frame;
if (self.contentSize.width > self.bounds.size.width) {
frame.origin.x = 0;
contentOffset.x = prevCenterPoint.x * self.zoomScale - roundf(self.bounds.size.width / 2);
if (contentOffset.x < 0) {
contentOffset.x = 0;
} else if (contentOffset.x > self.contentSize.width - self.bounds.size.width) {
contentOffset.x = self.contentSize.width - self.bounds.size.width;
}
}
if (self.contentSize.height > self.bounds.size.height) {
frame.origin.y = 0;
contentOffset.y = prevCenterPoint.y * self.zoomScale - roundf(self.bounds.size.height / 2);
if (contentOffset.y < 0) {
contentOffset.y = 0;
} else if (contentOffset.y > self.contentSize.height - self.bounds.size.height) {
contentOffset.y = self.contentSize.height - self.bounds.size.height;
}
}
zoomView.frame = frame;
self.contentOffset = contentOffset;
}
}
Bonus
I've created a working SMScrollView class (here is link to GitHub) implementing the above behavior and additional bonuses:
You can notice that in Photos app, zooming a photo, then scrolling it to one of its boundaries and then rotating the device does not keep the center point in its place. Instead it sticks the scrollView to that boundary. And if you scroll to one of the corners and then rotate, the scrollView will be stick to that corner as well.
In addition to adjusting contentOffset you may find that you also want to adjust the scrollView's zoomScale. For example, assume you are viewing a photo in portrait mode that is scaled to fit the screen size. Then when you rotate the device to the landscape mode you may want to upscale the photo to take advantage of the available space.
I have a photo gallery view that uses a UICollectionView with a UICollectionViewFlowLayout, it has pagingEnabled and scrolls horizontally showing only one view at a time.
Works great till I try to rotate it...
When I rotate the device, in willRotateToInterfaceOrientation:duration: I update the collectionView.contentOffset so it stays on the correct item and I resize the currentCell so it animates into the new dimensions. The problem is in the animation between the two states, the 'previous' orientation animates from the upper left corner AND flings into the view other cells. What is it I'm doing wrong such that the view being animated off the screen is FUBAR?
Here is what it looks like in action:
http://www.smugmug.com/gallery/n-3F9kD/i-BwzRzRf/A (ignore the choppy video, thats Quicktime's fault :p)
Here is my willAnimateRotationToInterfaceOrientation:duration:
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
[super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
// Update the flowLayout's size to the new orientation's size
UICollectionViewFlowLayout *flow = (UICollectionViewFlowLayout *)self.collectionView.collectionViewLayout;
if (UIInterfaceOrientationIsLandscape(toInterfaceOrientation)) {
flow.itemSize = CGSizeMake(self.collectionView.frame.size.width, self.collectionView.frame.size.height);
} else {
flow.itemSize = CGSizeMake(self.collectionView.frame.size.width, self.collectionView.frame.size.height);
}
self.collectionView.collectionViewLayout = flow;
[self.collectionView.collectionViewLayout invalidateLayout];
// Get the currently visible cell
PreviewCellView *currentCell = (PreviewCellView*)[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForRow:_currentIndex inSection:0]];
// Resize the currently index to the new flow's itemSize
CGRect frame = currentCell.frame;
frame.size = flow.itemSize;
currentCell.frame = frame;
// Keep the collection view centered by updating the content offset
CGPoint newContentOffset = CGPointMake(_currentIndex * frame.size.width, 0);
self.collectionView.contentOffset = newContentOffset;
}
As far as I'm aware I can't find any sample code anywhere that illustrates how to make a 'photo gallery' style collection view that rotates gracefully.
I struggled with this for quite a while, until I at least found this 'cosmetic workaround':
Add a full screen UIImageView with the current image (and correct auto layout constraints set) on top of the collectionView during rotation. Like so:
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration: (NSTimeInterval)duration
{
[self.collectionView.collectionViewLayout invalidateLayout];
// show a UIImageView with the current image on top of the collectionView
// to cover the ugly animation
self.imageViewOnTopOfCollectionView.image = [self imageForItemAtIndexPath:self.currentIndexPath];
self.imageViewOnTopOfCollectionView.hidden = NO;
// show a centered, very large 'fakeBackground' view on top of
// the UICollectionView, but below the UIImageView
self.fakeBackground.hidden = NO;
}
-(void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
// ... set correct contentOffset
// hide the fake views again
self.imageViewOnTopOfCollectionView.hidden = YES;
self.fakeBackground.hidden = YES;
}
A large 'fakeBackground' would be an extra improvement to prevent parts of the ugly collection view animation being visible outside this imageViews frame while it rotates. E.g. an oversized (larger than the view's bounds in all dimensions) UIView with the same background color as the collectionView, with a z-Index just between the collectionView and the imageView.
This work like a charm:
-(CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
return self.view.bounds.size;
}
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
int currentPage = collectionMedia.contentOffset.x / collectionMedia.bounds.size.width;
float width = collectionMedia.bounds.size.height;
[UIView animateWithDuration:duration animations:^{
[self.collectionMedia setContentOffset:CGPointMake(width * currentPage, 0.0) animated:NO];
[[self.collectionMedia collectionViewLayout] invalidateLayout];
}];
}
Adding to #Goodsquirrel's great answer, here is how I've implemented it using current iOS8 API's and Swift:
override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransitionToSize(size, withTransitionCoordinator: coordinator);
// show the dummy imageView
self.imageViewOnTopOfCollectionView.image = self.imageForItemAtIndex(self.currentIndex)
self.imageViewOnTopOfCollectionView.hidden = false;
coordinator.animateAlongsideTransition({ (context) -> Void in
// update the dummy imageView's frame
var frame:CGRect = self.imageViewOnTopOfCollectionView.frame;
frame.size = size;
self.imageViewOnTopOfCollectionView.frame = frame;
// update the flow's item size
if let flow = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
flow.itemSize = size;
}
// scroll to the current index
self.scrollToItem(self.currentIndex);
}, completion: { (context) -> Void in
// remove the dummy imageView
self.imageViewOnTopOfCollectionView.hidden = true;
});
}
I want to use a UIScrollView as my main container in the app, enabling me to swipe back and forth between subviews. To achieve this, I created a UIViewController subclass with a UIScrollView IBOutlet:
In the viewDidLoad method I construct the sub-pages:
for (int i= 0; i< pageCount; i++)
{
CGRect frame = self.scrollView.frame;
frame.origin.x = frame.size.width * i;
frame.origin.y = 0;
UIWebView* aWebView= [[UIWebView alloc] initWithFrame:frame];
[self.scrollView addSubview:aWebView];
}
When launching the app (portrait mode), everything works. That is, the UIWebViews are layed out side by side with the correct dimensions, and I can swipe back and forth between them.
When I rotate to landscape, it seems that neither the scrollview size nor the subviews are resized.
I don't know what I should do in order to resize the subviews and the scrollview itself, or at what point in code I should do anything, and I cant seem to find any examples for this.
Anyone know what to do?
[edit] Attempt to adjust sizes as suggested by mahboudz:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
self.scrollView.contentSize = CGSizeMake(self.scrollView.frame.size.width * pageCount, self.scrollView.frame.size.height);
for (int i= 0; i< pageCount; i++)
{
CGRect frame = self.scrollView.frame;
frame.origin.x = frame.size.width * i;
frame.origin.y = 0;
UIWebView* view= [[self.scrollView subviews] objectAtIndex:i];
view.frame= frame;
}
}
This kind of does what I want, but has the following issues:
1) one can see the subviews grow to correct screen size upon changing orientation
2) when the current page is, for example, page 2 of 5 pages, the page is not fully visible after orientation was changed, but is off-screen by like 40 pixels
3) I get strange effects depending on whether the app is launched in portrait or landscape mode (simulator), ill try to explain:
When the app is launched in portrait mode:
The shape/border of the subviews looks messed up/offscreen, see screenshots:
http://i53.tinypic.com/21jr76x.png
when I rotate to landscape, everything looks okay, scrolling works superb. even when I rotate back to portrait, everything is great now:
http://i55.tinypic.com/if3iiw.png
When the app is launchend in landscape mode:
I get the same messed up/offscreen glitches as in portrait mode
Switching back and forth between portrait and landscape fixes this for landscape mode
BUT: Portrait mode will have the subviews with the width of the landscape mode, thus subviews are too wide
I tried to fix 1) doing the code above in willRotateToInterfaceOrientation however it completely messed up the layout.
I fixed 2) by adding the following code to didRotateFromInterfaceOrientation:
// update the scroll view to the appropriate page
CGRect frame = self.scrollView.frame;
frame.origin.x = frame.size.width * self.currentPage;
frame.origin.y = 0;
[self.scrollView scrollRectToVisible:frame animated:NO];
Note: current page is determined in scrollViewDidScroll
I dont have any idea how to fix 3)
You would need to reset the frame size, content size and the content offset in order to get the subviews in a proper position.
CGFloat screenHeight =[UIScreen mainScreen].bounds.size.height;
CGFloat screenWidth =[UIScreen mainScreen].bounds.size.width;
self.scrollView.frame = CGRectMake(0, 0, screenWidth, screenHeight);
self.scrollView.contentSize = CGSizeMake(scrollView.frame.size.width * numberOfPages, self.scrollView.frame.size.height);
self.scrollView.contentOffset = CGPointMake(visiblePageBeforeRotation * self.scrollView.bounds.size.width, 0);
This code should be placed in the method
-(void)willRotateToInterfaceOrientation: (UIInterfaceOrientation)orientation duration:(NSTimeInterval)duration
Check as well the answer on this question:
Clean autorotation transitions in a paging UIScrollView
It has good example named Rotolling for rotating UIScrollView with paging enabled.
Hope this helps.
P.S: I am facing a problem on repositioning the center of the UIWebView on the rotation.
You need to implement viewWillRotate/viewDidRotate and make adjustments to our content size and orientation as needed.
I have seen several similar questions to this, but none that addresses my specific need. I want to be able to write a generic helper method that returns the maximum usable frame size for a UIView, taking into account whether the app has any combination of a status bar, navigation bar and/or tab bar as I find myself doing this all the time.
Method definition would be as an extension of UIScreen:
+ (CGRect) maximumUsableFrame;
Getting the size with or without the status bar can be got from the
[UIScreen mainScreen].applicationFrame
property, but I cannot figure out a way of determining if there is a navigation bar or tab bar present. I've thought about maintaining some global flags in my app delegate but this seems really clunky and stops the code being generic and re-usable. I have also considered passing a UIView as a parameter, getting the view's window, then the rootViewController and then seeing if the navigation controller property is set. If so then checking if the navigation controller is hidden. All very clunky if you ask me.
Any thoughts would be appreciated.
Dave
EDIT: Incorporating ideas from Caleb's answer in case this is of use to anyone else:
// Extension to UIViewController to return the maxiumum usable frame size for a view
#implementation UIViewController (SCLibrary)
- (CGRect) maximumUsableFrame {
static CGFloat const kNavigationBarPortraitHeight = 44;
static CGFloat const kNavigationBarLandscapeHeight = 34;
static CGFloat const kToolBarHeight = 49;
// Start with the screen size minus the status bar if present
CGRect maxFrame = [UIScreen mainScreen].applicationFrame;
// If the orientation is landscape left or landscape right then swap the width and height
if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) {
CGFloat temp = maxFrame.size.height;
maxFrame.size.height = maxFrame.size.width;
maxFrame.size.width = temp;
}
// Take into account if there is a navigation bar present and visible (note that if the NavigationBar may
// not be visible at this stage in the view controller's lifecycle. If the NavigationBar is shown/hidden
// in the loadView then this provides an accurate result. If the NavigationBar is shown/hidden using the
// navigationController:willShowViewController: delegate method then this will not be accurate until the
// viewDidAppear method is called.
if (self.navigationController) {
if (self.navigationController.navigationBarHidden == NO) {
// Depending upon the orientation reduce the height accordingly
if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) {
maxFrame.size.height -= kNavigationBarLandscapeHeight;
}
else {
maxFrame.size.height -= kNavigationBarPortraitHeight;
}
}
}
// Take into account if there is a toolbar present and visible
if (self.tabBarController) {
if (!self.tabBarController.view.hidden) maxFrame.size.height -= kToolBarHeight;
}
return maxFrame;
}
Use applicationFrame. [[UIScreen mainScreen] applicationFrame]
This is how I use it to crop stuffs in my screenshot.
Here is some sample code.
-(UIImage*) crop20PointsifStatusBarShowsUp
{
CGRect applicationFrame = [[UIScreen mainScreen] applicationFrame]; //Look at this
float sizeOfStatusBarVar= applicationFrame.origin.y;
if ([BGMDApplicationsPointers statusBarShowUp])
{
CGRect newSize = CGRectMake(0, sizeOfStatusBarVar, self.size.width, self.size.height-sizeOfStatusBarVar);
UIImage * newImage = [self cropUIImageWithCGRect:newSize];
return newImage;
}
else{
return [self copy];
}
}
I think you're putting your method in the wrong place. UIScreen knows about the screen, but it doesn't (and shouldn't) know anything about what's displayed on the screen. It's the view controller that's responsible for managing the view, so that's where the method belongs. Furthermore, since the max frame depends on the configuration of a particular view controller, this should be an instance method and not a class method. I think you should add a category to UIViewController with the method:
- (CGRect) maximumUsableFrame;
It's still fairly generic, available to all view controllers, but at the same time has access to view controller properties like navigationController and tabBarController.
To get this to work had to use application bounds and orientation switch to figure out from which side the status bar is supposed to be removed. This is a copy and tweak of the originally posted code. I made a quick app that runs this algorithm through the combinations of having navbar/tabbar/none with a status bar and it worked in all the scenarios.
- (CGRect) maxFrame
{
// Start with the screen size minus the status bar if present
CGRect maxFrame = [UIScreen mainScreen].applicationFrame;
// the glass screen size
CGRect maxBounds = [UIScreen mainScreen].bounds;
NSLog(#"MaxFrame for %d: (%f, %f, %f, %f)", self.interfaceOrientation, maxFrame.origin.x, maxFrame.origin.y, maxFrame.size.width, maxFrame.size.height);
NSLog(#"MaxBounds for %d: (%f, %f, %f, %f)", self.interfaceOrientation, maxBounds.origin.x, maxBounds.origin.y, maxBounds.size.width, maxBounds.size.height);
// figure out the offset of the status bar
CGFloat statusBarOffset;
switch (self.interfaceOrientation) {
case UIInterfaceOrientationPortrait:
statusBarOffset = maxFrame.origin.y;
break;
case UIInterfaceOrientationPortraitUpsideDown:
statusBarOffset = maxBounds.size.height - maxFrame.size.height;
break;
case UIInterfaceOrientationLandscapeRight:
case UIInterfaceOrientationLandscapeLeft:
statusBarOffset = maxBounds.size.width - maxFrame.size.width;
break;
}
// If the orientation is landscape left or landscape right then swap the width and height
if (UIInterfaceOrientationIsLandscape(self.interfaceOrientation)) {
CGFloat temp = maxBounds.size.height;
maxBounds.size.height = maxBounds.size.width;
maxBounds.size.width = temp;
}
// apply status bar to the top of the view
maxBounds.origin.y = statusBarOffset;
maxBounds.size.height -= statusBarOffset;
// Take into account if there is a navigation bar present and visible (note that if the NavigationBar may
// not be visible at this stage in the view controller's lifecycle. If the NavigationBar is shown/hidden
// in the loadView then this provides an accurate result. If the NavigationBar is shown/hidden using the
// navigationController:willShowViewController: delegate method then this will not be accurate until the
// viewDidAppear method is called)
if (self.navigationController && !self.navigationController.navigationBarHidden) {
NSLog(#"has nav bar");
maxBounds.size.height -= self.navigationController.navigationBar.frame.size.height;
maxBounds.origin.y += self.navigationController.navigationBar.frame.size.height;
}
// Take into account if there is a toolbar present and visible
if (self.tabBarController && !self.tabBarController.view.hidden) {
maxBounds.size.height -= self.tabBarController.tabBar.frame.size.height;
NSLog(#"has tab bar");
}
NSLog(#"result for %d: (%f, %f, %f, %f)", self.interfaceOrientation, maxBounds.origin.x, maxBounds.origin.y, maxBounds.size.width, maxBounds.size.height);
return maxBounds;
}