usable view size after status bar and navigation bar - ios

I am writing an iPad app that needs to know the usable area of the view for drawing purposes. The view is added into a Navigation controller, so I have the status bar plus the navigation controller both taking up a certain number of pixels. My app happens to be in landscape mode, although I don't think that's relevant.
I am able to get the correct view size AFTER rotation using didRotateFromInterfaceOrientation. But I can't figure out how to do it without the screen being rotated.
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
[self.view setFrame:CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
NSLog(#"drfi %d %d", (int)self.view.frame.size.width, (int)self.view.frame.size.height);
}
^^ that works after rotation. Not before. Can't figure out how to get accurate numbers. And I REALLY don't want to hard wire this.
I will also need this function to be device independent -- it should work on the NEW iPad as well as the older iPad resolutions. I can handle the scaling issues once I know the exact usable area. Why is this so hard? Help!!

I don't think you need to specify your frame's view within the didRotateFromInterfaceOrientation what i will suggest instead is setting some properties to your view autoresizing mask so that it automatically resize itself according to your view orientation.
By setting this for example to your view when your view is loaded (viewDidLoad method):
self.view.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth;
you specify that your view will change its width and height automatically and can get the right values you need to get from there.
You should read this: http://developer.apple.com/library/ios/#documentation/WindowsViews/Conceptual/ViewPG_iPhoneOS/CreatingViews/CreatingViews.html#//apple_ref/doc/uid/TP40009503-CH5-SW1
for a better understanding of views in iOS
EDIT
Also you probably want to spot what is the orientation of your device which can be accomplish with [[UIApplication sharedApplication] statusBarOrientation];

Your application looks like: there is a start up view, then in this view you will load and add a main view into window, right? Then you should do as below in your main view:
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
CGRect frame = self.view.frame;
frame.origin.y = frame.origin.y + 20.0;
self.view.frame = frame;
}
return self;
}

Try this.
CGRect frame = [UIScreen mainScreen].bounds;
CGRect navFrame = [[self.navigationController navigationBar] frame];
/* navFrame.origin.y is the status bar's height and navFrame.size.height is navigation bar's height.
So you can get usable view frame like this */
frame.size.height -= navFrame.origin.y + navFrame.size.height;

You can get this dynamically by combining an instance method with a category method:
Instance method:
This assumes that your view controller (self) is embedded within a navigation controller.
-(int)getUsableFrameHeight {
// get the current frame height not including the navigationBar or statusBar
return [MenuViewController screenHeight] - [self.navigationController navigationBar].frame.size.height;
}
Class category method:
+(CGFloat)screenHeight {
CGFloat screenHeight;
// it is important to do this after presentModalViewController:animated:
if ([[UIApplication sharedApplication] statusBarOrientation] == UIDeviceOrientationPortrait ||
[[UIApplication sharedApplication] statusBarOrientation] == UIDeviceOrientationPortraitUpsideDown){
screenHeight = [UIScreen mainScreen].applicationFrame.size.height;
} else {
screenHeight = [UIScreen mainScreen].applicationFrame.size.width;
}
return screenHeight;
}
The above will consistently give you the usable frame height after the status bar and navigation bar have been removed, in both portrait and landscape.
Note: the class method will automatically deduct the 20 pt for the status bar - then we just subtract the navigation header variable height (32 pt for landscape, 44 pt for portrait).

Related

iOS in-Call indicator is pushing down view/content, modifying root view `frame`

I have a problem that my root view (the UIViewController view) is being pushed down by the in-call indicator: window.rootViewController.view.frame is being modifeid (Y is set to 20). As I respond to did/willStatusBarFrameChange on my own, I don't want this behaviour.
I'm looking for the property, or setup, that prevents the modification of the frame in response to an in-call status bar. I use other APIs to respond to changes in the top/bottom frames and iPhone X safe areas.
I've tried things like autoResizingMask, extendedLayoutIncludesOpaqueBars, edgesForExtendedLayout, viewRespectsSystemMinimumLayoutMargins but can't get anything working.
If relevant, the view is also animating down, indicating it's not some side-effect but an intended behaviour somewhere.
I've read many reports of similar behaviour but have yet to figure out if they actually resolved it and/or what the solution actually was (each solution appears to address a slightly different problem).
Related questions: Prevent In-Call Status Bar from Affecting View (Answer has insufficient detail), Auto Layout and in-call status bar (Unclear how to adapt this)
--
I can't provide a simple reproduction, but the portions of code setting up the view looks something like this:
Window setup:
uWindow* window = [[uContext sharedContext] window];
window.rootViewController = (UIViewController*)[[UIApplication sharedApplication] delegate];
[window makeKeyAndVisible];
Our AppDelegate implementation (relevant part)
#interface uAppDelegate : UIViewController<#(AppDelegate.Implements:Join(', '))>
...
#implementation uAppDelegate
- (id)init
{
CGRect screenBounds = [UIScreen mainScreen].bounds;
uWindow* window = [[uWindow alloc] initWithFrame:screenBounds];
return self;
}
We assign our root view to the above delegate, the UIViewController's .view property.
#interface OurRootView : UIControl<UIKeyInput>
UIControl* root = [[::OurRootView alloc] init];
[root setUserInteractionEnabled: true];
[root setMultipleTouchEnabled: true];
[root setOpaque: false];
[[root layer] setAnchorPoint: { 0.0f, 0.0f }];
// some roundabout calls that make `root` the `rootViewController.view = root`
[root sizeToFit];
The goal is that OurRootView occupies the entire screen space at all times, regardless of what frames/controls/margins are adjusted. I'm using other APIs to detect those frames and adjust the contents accordingly. I'm not using any other controller, view, or layout.
It's unclear if there is a flag to disable this behaviour. I did however find a way that negates the effect.
Whatever is causing the frame to shift down does so by modifying the frame of the root view. It's possible to override this setter and block the movement. In our case the root view is fixed in position, thus I did this:
#implementation OurRootView
- (void)setFrame:(CGRect)frame;
{
frame.origin.y = 0;
[super setFrame:frame];
}
#endf
This keeps the view in a fixed location when the in-call display is shown (we handle the new size ourselves via a change in the statusBarFrame and/or safeAreaInsets). I do not know why this also avoids the animation of the frame, but it does.
If for some reason you cannot override setFrame you can get a near similar seffect by overriding the app delegate's didChangeStatusBarFrame and modifying the root view's frame (setting origin back to 0). The animation still plays with this route.
I hope I understand your problem: If you have some indicator like incall, or in my case location using by maps. You need to detect on launching of the app that there is some indicator and re-set the frame of the whole window. My solution for this:
In didFinishLaunchingWithOptions you check for the frame of the status bar, because incall is the part of status bar.
CGFloat height = [UIApplication sharedApplication].statusBarFrame.size.height;
if (height == 20) {
self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
}
else {
CGRect frame = [[UIScreen mainScreen] bounds];
frame.size.height = frame.size.height - height +20;
frame.origin.y = height-20;
self.window = [[UIWindow alloc] initWithFrame:frame];
}
You can listen to the notification UIApplicationDidChangeStatusBarFrameNotification in your view controller(s) to catch when the status bar has changed. Then you adjust your view controller's main view rectangle to always cover the entire screen.
// Declare in your class
#property (strong, nonatomic) id<NSObject> observer;
- (void)viewDidLoad {
[super viewDidLoad];
_observer = [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidChangeStatusBarFrameNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
CGFloat newHeight = self.view.frame.size.height + self.view.frame.origin.y;
self.view.frame = CGRectMake(0.0, 0.0, self.view.frame.size.width, newHeight);
}];
}
-(void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:_observer];
}
I tried it on various models, and it works fine, as far as I can tell. On iPhone X the notification is not posted since it does not alter the status bar height on calls.
There is also a corresponding UIApplicationWillChangeStatusBarFrameNotification which is fired before the status bar changes, in case you want to prepare your view in some way.

Best method to resize UIView when rotated to landscape and back

Since I am very new to ios programming I have more of a general-design question.
I have a ViewController which contains a GraphView (UIScrollView + UIView) which works fine. When I am rotating to landscape I want the GraphView to resize its height to the display height (so it fills the whole screen) but only 300pts when in portrait.
What I did so far is implementing viewWillLayoutSubviews in the ViewController and resetting the constraints:
- (void)viewWillLayoutSubviews{
_graphViewHeightConstraint.constant = ([[UIDevice currentDevice] orientation] == UIDeviceOrientationPortrait) ? 300:[[UIScreen mainScreen] bounds].size.height-self.navigationController.navigationBar.frame.size.height - 2*_distanceToTopView.constant;
}
and in GraphView.m:
- (void)layoutSubviews{
kGraphHeight = self.frame.size.height;
[self setNeedsDisplay];
}
(because I need the variable kGraphHeight in the code to draw the Graph). This does not seem like a very elegant solution so I wanted to ask what the better way would be? Many thanks for your inputs :)
In GraphView.m
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
kViewWidth = <GET_SCREEN_WIDTH_HERE>;
kViewHeight = <GET_SCREEN_HEIGHT_HERE>;
[self updateViewDimensions];
}
and updateViewDimensions method will set the frame of UIScrollView and UIView
- (void)updateViewDimensions
{
scrollView.frame = self.view.frame;
yourView.frame = CGRectMake(kViewXStartsFrom, kViewYStartsFrom, kViewWidth, kViewHeight);
}
after rotating view to Landscape viewDidLayoutSubviews will be called.
It's working for me.

Shift frame inside UIPopoverController

My universal app uses NIBs for its settings screens. I'd like to use the same NIBs for both iPhone and iPad.
Thus on iPad, I use a UIPopoverController in the MainViewController and for settings, simply display the iPhone-sized NIBs, to show what is called the SettingsViewController. The popover is sized 320x460 points.
This causes a problem, because the iPhone version draws a number of things above the status bar programmatically, and for the iPad version this is not necessary. Current situation on iPad:
As you can see, there's a big empty space above the "Settings" title. Thus what I want, is to shift the view controller up about 20 points, inside the popover:
The popover is instantiated as follows in the MainViewController:
settingsPopoverController = [[UIPopoverController alloc] initWithContentViewController:popoverNavigationController];
settingsPopoverController.popoverContentSize = CGSizeMake(320, 460);
settingsPopoverController.delegate = self;
popoverNavigationController.navigationBarHidden = YES;
In the SettingsViewController, I set the frame as follows:
- (void)viewDidLoad
{
self.contentSizeForViewInPopover = CGSizeMake(320, 460);
}
And later in the SettingsViewController, I try to create an offset as follows:
- (void)viewWillLayoutSubviews
{
// shift it up
CGRect frame = self.view.frame;
frame.origin.y = -20;
[self.view setFrame:frame];
}
This does not shift the content up a bit. How to go about?
To clarify: I want to move down the "viewport" that the popover shows.
Try to:
myPopover.autoresizingMask=UIViewAutoresizingNone;
Or:
myPopover.autoresizingMask=UIViewAutoresizingFlexibleHeight || UIViewAutoresizingFlexibleWidth;
You can also try to put your code in -(void)viewDidLayoutSubviews method.
If this answers did not help, try set popoverLayoutMargins (property) instead of setFrame: for example:
popover.popoverLayoutMargins=UIEdgeInsetsMake (
CGFloat 50, //top
CGFloat 50,//left
CGFloat 50,//bottom
CGFloat 50//right
);

Is it possible to access properties of an iOS system animation?

Is it possible to get the animation properties (speed and easing curve) for a built in iOS system animation? Specifically, the UIStatusBarAnimationSlide when you hide or show the status bar. Right now I'm just eyeballing it and have come up with a good match: .35 seconds using the default animation curve. This works fine, but Apple is liable to change an animation like this in a future iOS update and it would be nice to match it exactly and not rely on hard-coded values I came up with myself.
For what it's worth, here is the method my view controller is calling when I tap the view to hide the status bar and resize the view to fill the screen.
-(void)tappedView:(UIGestureRecognizer *)gestureRecognizer
{
UIApplication *app = [UIApplication sharedApplication];
// First, toggle the visibility of the status bar
[[UIApplication sharedApplication] setStatusBarHidden:![app isStatusBarHidden] withAnimation:UIStatusBarAnimationSlide];
// Then scale this view controller's view, attempting to match the built-in
// UIStatusBarAnimationSlide animation
[UIView animateWithDuration:.35
animations:^{
self.view.frame = [UIScreen mainScreen].applicationFrame;
}];
}
As an aside, I'm surprised I couldn't find a built in way to handle resizing a VC's view when the status bar is hidden. After all, if the status bar doubles its height when a call is in progress, the view resizes automatically. Tell me I'm missing something and there's a way to get the view to grow automatically, too.
Here's a chuck of code I use in my app:
- (void)application:(UIApplication *)application willChangeStatusBarFrame:
(CGRect)oldStatusBarFrame {
[UIView animateWithDuration:0.355f animations:^{
if(floating_point_values_are_equal(oldStatusBarFrame.size.height, 20.0f)) {
for(UIViewController* VC in self.tabBarController.viewControllers) {
UIView* view = VC.view;
[view setTransform:CGAffineTransformMakeScale(1.0f, 1.0f)];
}
} else {
for(UIViewController* VC in self.tabBarController.viewControllers) {
UIView* view = VC.view;
CGFloat ratio = (view.frame.size.height - 20) / view.frame.size.height;
[view setTransform:CGAffineTransformMakeScale(1.0f, ratio)];
}
}
}];
}
It basically scales the entire app depending on the new screen dimensions. It only works because the scale ratio is not a big change- doing this for the new iPhone screen would not look right.

Programmatically determine the maximum usable frame size for a UIView

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;
}

Resources