We have a UISplitViewController and under condition X, we need to display a UIPopover from one of the UIBarButtonItem of the Master view.
Supposedly, in order to have the frame/layout correct, we do this code from the Master view controller's viewDidLoad event. Somehow the first time the UISplitViewController is shown, the frame of the Master is 1024x724 whereas we'd expect it to be 320x724. As a result, the call to [UIPopover presentFromBarButtonItem:] uses a wrong referential and since it's a right BarButtonItem, the popover appears all the way to the right of the screen (at about x = 980px)
If we delay the displaying by a split second (via a Timer/delay, sooo dirty) then it's all fine.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
CGRect masterViewFrame = self.view.frame;
NSLog(#"Master View Frame: %#", NSStringFromCGRect(masterViewFrame));
if (someCondition) {
[self showPopover:self.theBarButton];
}
}
The NSLog here shows 1024x724 # 0x0
Thoughts?
You can do something like this:
//Check if you really are in a UISplitViewController
CGRect frame = self.view.frame; //this is the default value
if(self.splitViewController)
{
//you are trying to access the frame of this VC's view via the splitViewController
//this should return the correct size
frame = [[[self.splitViewController.viewControllers objectAtIndex:0] view] frame];
}
Related
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.
When a view controller's view is first shown I want to run an animation in which all elements in the view controller slide from outside the bottom of the screen to their natural positions. To achieve this, I do subview.frame.origin.y += self.view.frame.size.height in viewDidLayoutSubviews. I also tried viewWillAppear, but it doesn't work at all. Then I animate them up to their natural positions with subview.frame.origin.y -= self.view.frame.size.height in viewDidAppear.
The problem is that viewDidLayoutSubviews is called several times throughout the view controller's lifespan. As such, when things like showing the keyboard happen all my content gets replaced outside the view again.
Is there a better method for doing this? Do I need to add some sort of flag to check whether the animation has already run?
EDIT: here's the code. Here I'm calling prepareAppearance in viewDidLayoutSubviews, which works, but viewDidLayoutSubviews is called multiple times throughout the controller's life span.
- (void)viewDidLayoutSubviews
{
[super viewDidLayoutSubviews];
[self prepareAppearance];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self animateAppearance];
}
- (NSArray *)animatableViews
{
return #[self.createAccountButton, self.facebookButton, self.linkedInButton, self.loginButton];
}
- (void)prepareAppearance
{
NSArray * views = [self animatableViews];
NSUInteger count = [views count];
for (NSUInteger it=0 ; it < count ; ++it) {
UIView * view = [views objectAtIndex:it];
CGRect frame = view.frame;
// Move the views outside the screen, to the bottom
frame.origin.y += self.view.frame.size.height;
[view setFrame:frame];
}
}
- (void)animateAppearance
{
NSArray * views = [self animatableViews];
NSUInteger count = [views count];
for (NSUInteger it=0 ; it < count ; ++it) {
__weak UIView * weakView = [views objectAtIndex:it];
CGRect referenceFrame = self.view.frame;
[UIView animateWithDuration:0.4f
delay:0.05f * it
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGRect frame = weakView.frame;
frame.origin.y -= referenceFrame.size.height;
[weakView setFrame:frame];
}
completion:^(BOOL finished) {
}];
}
}
If you need to animate something when view will appear and then not touch subviews later, I would suggest the following:
Don't change/touch viewDidLayoutSubviews
Add logic to move elements outside the screen (to their initial position before animation) in viewWillAppear
Add logic to animate elements into their proper position in viewDidAppear
UPDATE:
If you're using auto-layout (which is very good thing), you can't animate views by changing their frames directly (because auto-layout would ignore that and change them again). What you need to do is to expose outlets to constraints responsible for Y-position (in your case) and change that constraints rather then setting frames.
Also don't forget to include call to [weakView layoutIfNeeded] after you update constraints in the animation method.
I did both things in viewDidAppear:. It seems that when viewDidAppear: is called, the view is not actually visible, but about to. So the UI elements never show in their initial position if they are replaced there.
I'm trying to get the offset of my viewControllers' views relative to the top of the screen.
I thought I could convert the origin of the view to the window's coordinates so I tried something like this in my viewControllers:
CGPoint basePoint = [self.view convertPoint:self.view.frame.origin toView:nil];
CGFloat offset = basePoint.y;
It works as expected in some cases, however in other cases it returns different values even when the parameters are the same.
Any ideas on what's going on behind the scenes of this convertPoint:toView: that might be resulting in different return values?
Or if you have any other suggestions to get the offset of my viewControllers' views relative to the top of the screen it would be very much appreciated!
Thanks
You are converting your point to a point in a nil view.
The initial window, has a rootViewController which has a view and so you can access to that view:
UIView *firstView = [[[(YourClassAppDelegate *)[UIApplication sharedApplication] window] rootViewController] view];
CGPoint basePoint = [self.view convertPoint:self.view.frame.origin toView:firstView];
CGFloat offset = basePoint.y;
remember to import in the implementation, your class of the app delegate.
I end up crawling up the chain of superviews and adding up all the offsets.y from each view. Not sure if it's the best approach, especially performance wise, but for now it works.
UIView *view = self.view;
CGFloat offset = view.frame.origin.y;
while (view.superview != nil) {
offset += view.superview.frame.origin.y;
view = view.superview;
}
try change this
CGPoint basePoint = [self.view convertPoint:self.view.frame.origin toView:nil];
to
CGPoint basePoint = [self.view.superview convertPoint:self.view.frame.origin toView:nil];
Your two views must have common superview (for example UIWindow), otherwise you'll get weird results.
This might happen when you try to use UIViewController's view in -viewDidLoad with convertPoint, because view is not yet added to the view hierarchy.
- (void)viewDidLoad
{
[super viewDidLoad];
NSLog(#"%#", self.view.superview); // nil, not added to view hierachy
// If you try to convert point using self.view (or any self.view subview),
// and any other view, which is not self.view subview (for example, UIWindow),
// it will fail, because self.view is not yet added to the view hierachy.
}
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
);
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).