3

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.

edA-qa mort-ora-y
  • 30,295
  • 39
  • 137
  • 267
  • You didn't use autolayout? You instantiated views from code? – Stefan Feb 05 '18 at 13:06
  • Yes, this is all coded by hand in ObjC. We need control over the root views. – edA-qa mort-ora-y Feb 05 '18 at 13:30
  • I had the same problem. If I understand well, I will send you the solution. – Stefan Feb 05 '18 at 13:37
  • You really have to use autolayout (ie, constraints), edA. You can't realistically make an iOS app today without using autolayout. Everything is constraint based now, and, things won't really work unless you are using constraints. You mention you "control the root views". We completely control the root views in every app we work on (as you know, you have to do it for any sort of complex UX interaction). I hope it helps in the long run! – Fattie Feb 06 '18 at 13:15
  • 1
    “The problem is that my view itself is being moved down within the controller, so I can't draw underneath it.” what does that even mean? A controller is not a view so a view can’t move within it. And what is “draw underneath”? Your app moving down when the in call bar appears is normal and expected. What’s the problem? – matt Feb 06 '18 at 13:23
  • I agree with @matt. This question is super confusing. I think a "this is what I get" / "this is what I would want instead" approach (with visuals) would clarify a lot of things... Because right now, it feels like you're fighting the layout system... – Alladinian Feb 06 '18 at 13:38
  • @Alladinian I am fighting the layout system. I'm adding a UIView (UIControl) to the rootViewController and the in-call status display is modifying its `frame`. This is the extent of the layout I have -- no further controllers, or layout controls. – edA-qa mort-ora-y Feb 06 '18 at 13:51
  • @edA-qamort-ora-y But you still haven't shown any code, any explanation of how you are "adding a UIView to the rootViewController" (which is a meaningless phrase, since a view controller can't have views "added" to it), any screen shots, any explanation of how what is happening is anything other than normal. If you want to throw 100 pts of rep down the drain, fine, but it would be better to spend it on a good answer, and you won't get that until you improve your question. – matt Feb 06 '18 at 13:53
  • I've clarified some details, I'll see if I can pull out the class/instance structure from the code. – edA-qa mort-ora-y Feb 06 '18 at 13:57
  • @matt Clarified more. – edA-qa mort-ora-y Feb 06 '18 at 14:12
  • 1
    "a roundabout call to `rootViewController.view = root`" But you're aware that that's completely illegal? You may not _assign_ a view to a view controller (except within its implementation of `loadView`, of course). There are ways in which a view controller _gets_ its view, and direct assignment is not one of them. Everything you're doing is _so_ offbeat, so completely outside the pale, that I'm amazed it has ever worked at all. This makes it very difficult to sympathize. – matt Feb 06 '18 at 14:32
  • Perhaps I should not have said assignment there, I didn't fully trace that code path, more like it properly becomes the `view`. I understand what I'm doing is off-beat, but it's not illegal, it's supported, and other than the in-call display it works fine. Even if offbeat, it's still a concrete technical question about the frame being modified by "something" that could be answered. – edA-qa mort-ora-y Feb 06 '18 at 14:40
  • Well, one problem with instantiating view from the code is that you are getting frame of the UIMainScreen, in that part is not calculated statusBar with extended view(in this case incall view), so the whole view will not fit into the screen, it will cut exactly for the height of the extended statusBar. – Stefan Feb 07 '18 at 15:07

3 Answers3

1

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.

edA-qa mort-ora-y
  • 30,295
  • 39
  • 137
  • 267
0

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];
    }
Stefan
  • 1,283
  • 15
  • 33
  • I was hoping for an API, or flag, that would disable the behaviour. Using statusBarFrame this way could result in sizing errors if the OS behaviour changes. – edA-qa mort-ora-y Feb 06 '18 at 07:42
  • Well, using storyboards will solve this problem. That is the only way that is not hackish. – Stefan Feb 06 '18 at 11:16
0

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.

LGP
  • 4,135
  • 1
  • 22
  • 34