Slide UIPageViewController page over the status bar - ios

I am trying to find a clean way to handle UIPageViewController and the status bar. I noticed Snapchat does it perfectly by sliding the viewcontroller OVER the status bar when you are sliding to a new page that does not show the status bar. It looks like this...
Does anyone know how this is being done? The only way I can think of is by using a different UIWindow, but how would you implement a UIPageViewController with multiple UIWindows? If that is even what is being done. Otherwise how is this effect being achieved?

Basically create a UIViewController, put the UIPageViewController inside that. Make the size of UIVC the size of iphone screen. Then set the appdelegate.window.rootviewcontroller as your UIViewControllers
Here is a great example repo for this task: https://github.com/goktugyil/EZSwipeController

Ok so this is actually done using some clever tricks.
Basically it's not actually moving the status bar, its moving an image of the status bar.
Disclaimer: I YOLO'd this implementation so I make no guarantees on if it'll work out of the box
Starting with iOS7 you can take a picture using
UIView *screen = [[UIScreen mainScreen] snapshotViewAfterScreenUpdates:NO];
There's a bit more useful information I saw here. But basically the status bar is cropped from the picture and added to a UIWindow with a windowLevel above UIWindowStatusLevelBar that will mask the real status bar. Something like:
UIWindow *statusBarWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
statusBarWindow.windowLevel = UIWindowLevelStatusBar;
statusBarWindow.hidden = NO; // fun fact you don't have to add a UIWindow to anything, just setting hidden = NO should display it
statusBarWindow.backgroundColor = [UIColor clearColor]; // I have no idea if this is necessary but since it's now visible you def don't want it to have a color
...
// The view that will hold the screen shot
UIView *statusBarView = [[UIView alloc] initWithFrame:[UIApplication sharedApplication].statusBarFrame];
statusBarView.clipsToBounds = YES;
// Now we add the screen shot we took to this status bar view
[statusBarView addSubview:screen]; // screen is the UIView from previous code block
// Now add this statusBarView with the image to the window we created
[statusBarWindow addSubview:statusBarView];
Now the rest really depends on what your implementation is looking like but from here you really just need to handle moving the view with either a pan, or whatever action is causing the new view to come in through the side:
// you're going to want to hide the status bar when this action starts so that your new window is visible
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
// then inside whatever method is handling the sliding you're going to adjust the frame of the statusBarView
// this can be done explicitly by setting a new frame, or animating its x position, or whatever
// if you're doing it explicitly something like:
CGFloat offset = self.viewThatIsScrolling.contentOffset.x; // may be negative depending on direction that is being swiped
statusBarView.frame = CGRectMake(offset, 0, self.statusBarView.frame.width, 20.0f);
// or maybe even
statusBarView.transform = CGAffineTransformMakeTranslation(offset -previousOffsetAmount, 0); // you only want it to move over by whatever new amount so it can match up, this will have to be tracked somehow if you go this route
...
// and finally once the 'sliding' is done don't forget to remove the screenshot and unhide the status bar
// (can check by looking at the offset value from earlier or as a callback after animation, or whatever)
[[UIApplication sharedApplication] setStatusBarHidden:NO withAnimation:UIStatusBarAnimationNone];
[statusBarView removeFromSuperview];
statusBarView = nil;
Also note this doesn't handle orientation changes or anything (the fixed frame might cause it to not work). you could probably use autoResizingMask or something

Related

Change status bar alpha

In the Snapchat app, the status bar changes alpha when a table view is dragged down to reload.
This is the normal status bar
This is what happens when the table view is dragged down
How is the status bar alpha changed? And how is the frame changed? Originally I thought it was a snapshot, but the clock changes as it normally should.
This should do the trick for the fade animation:
/* Swift 3 */
let statusBarWindow = UIApplication.shared.value(forKey: "statusBarWindow") as? UIWindow
UIView.animate(withDuration: 2) {
statusBarWindow?.alpha = 0.2
}
/* Objective-C */
UIWindow *statusBarWindow = (UIWindow *)[[UIApplication sharedApplication] valueForKey:#"statusBarWindow"];
[UIView animateWithDuration:2.f
animations:^{ [statusBarWindow setAlpha:0.2]; }
completion:nil];
You can also set statusBarWindow's frame and move it how you want. Have fun ;]
They are most likely either grabbing the window the status bar is in and animating that or they are placing a window above the status bar with a view and animating that. I don't have the app to check but it has to be either of these two methods. To place a window above the status bar I think it is window.windowLevel = UIWindowLevelStatusBar + 1 so that it will be above your status bar. If they are grabbing the reference you have to loop through the application windows to find it and I have never tried to find the Statusbar window but I have grabbed the keyboard window. If the clock is indeed being pulled down then it could be a combination of both the getting the window the status bar is in and adding a window above and animating both. Facebook grabs the keyboard window to scroll it up in Facebook Messenger. Hope this helps you.
Edit:
I found an example of getting the status bar window here.
Also I would recommend using CAAnimations and avoid really changing anything. Just use the animations to give that appearance.

Handling In-Call Status Bar with Custom Modal Presentation

The Problem
I've noticed some strange behavior when presenting a UINavigationController (with a root view controller, already pushed, naturally) with UIViewControllerAnimatedTransitioning during a phone call.
If the in-call status bar is enabled after the the navigation controller is presented, the navigation controller shifts its view down as expected. But when the call is ended, the controller does not shift its view back up, leaving a 20p gap under the status bar.
If the in-call status bar is enabled before presenting the controller, the controller does not account for the status bar at all, leaving 4p of the 44p-high navigation bar peeking out from under the 40p status bar. When the call is ended, the controller shifts its view down to accommodate the normal 20p status bar.
*note: this was tested on the simulator, due to the ease of enabling/disabling the in-call status bar, but testers have observed this phenomenon on actual phones.
My (Partial) Workaround
I hacked around the issue by adjusting the frame of the controller during presentation, if the status bar was an abnormal height:
#interface CustomAnimationController : NSObject <UIViewControllerAnimatedTransitioning>
#end
#implementation CustomAnimationController
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
UIViewController *toController = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
UIView *container = [transitionContext containerView];
CGRect frame = [transitionContext finalFrameForViewController:toController];
if (CGRectEqualToRect(frame, CGRectZero))
{
// In my experience, the final frame is always a zero rect, so this is always hit
UIEdgeInsets insets = UIEdgeInsetsZero;
// My "solution" was to inset the container frame by the difference between the
// actual status bar height and the normal status bar height
insets.top = CGRectGetHeight([UIApplication sharedApplication].statusBarFrame) - 20;
frame = UIEdgeInsetsInsetRect(container.bounds, insets);
}
toController.view.frame = frame;
[container addSubview:toController.view];
// Perform whiz-bang animation here
}
#end
This solution ensures that the navigation bar is below the status bar, but the navigation controller still fails to shift itself back up when the call is ended. So the app is at least usable, but there is an ugly 20p gap above the navigation bar after a call ends.
Is There a Better Way?
Am I missing some critical step to ensure that the navigation controller accounts for the in-call status bar on its own? It works just fine when presented with the built-in modal presentation style.
In my opinion this smacks of a UIKit bug — after all, the navigation controller seems to receive the UIApplicationWillChangeStatusBarFrameNotification (see second point of The Problem). If anyone else has encountered this problem and has found a better way, I would greatly appreciate a solution.
I have spent far too much time on over coming the status bar height issue and have come up with a general solution that works for me and I think will work for your situation as well.
First, a couple things that are odd about the status bar.
It's normally 20 points tall and the screen is normally 568 points tall
While "in-call", the status bar is 40 points high and the screen is 548 points tall
While the status bar is hidden, the status bar is 0 points tall and the screen is 568 points tall
If the status bar changes but you don't update the height of the screen then the calculations will be off, and this can be seen in some pretty big name (and even default) applications.
So, the solution that I've come up with is two fold: 1. Create a macro to get the adjusted screen height 2. Register a notification to update the view when the status bar changes.
Here are the macros, I'd recommend putting these in your prefix file
#define kScreenWidth [UIScreen mainScreen].bounds.size.width
#define kStatusBarHeight (([[UIApplication sharedApplication] statusBarFrame].size.height == 20.0f) ? 20.0f : (([[UIApplication sharedApplication] statusBarFrame].size.height == 40.0f) ? 20.0f : 0.0f))
#define kScreenHeight (([[UIApplication sharedApplication] statusBarFrame].size.height > 20.0f) ? [UIScreen mainScreen].bounds.size.height - 20.0f : [UIScreen mainScreen].bounds.size.height)
Additionally, here's the notification center call that I've found works for me 100% of the time the status bar changes.
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self.view selector:#selector(layoutSubviews) name:UIApplicationWillChangeStatusBarFrameNotification object:nil];
I had the same issue and the problem was with the view that I was presenting which was automatically adjusted by 20pt because of in-call bar. However since I insert the view into container, I had to reset the frame to match container's bounds.
- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext {
UIView* destinationView = [transitionContext viewForKey:UITransitionContextToViewKey];
UIView* container = transitionContext.containerView;
// Make sure destination view matches container bounds
// This is necessary to fix the issue with in-call bar
// which adjusts the view's frame by 20pt.
destinationView.frame = container.bounds;
// Add destination view to container
[container insertSubview:destinationView atIndex:0];
// [reducted]
}
Just set the frame in layoutSubviews as it is called when the status bar height changes. In general as a rule of thumb, do all frame setting in layoutSubviews only.
Here is a workaround I put in my app delegate that fixed the problem for me:
- (void)application:(UIApplication *)application willChangeStatusBarFrame:(CGRect)newStatusBarFrame
{
if (newStatusBarFrame.size.height < 40) {
for (UIView *view in self.window.subviews) {
view.frame = self.window.bounds;
}
}
}
I had the same issue and I fixed it by call update frame view, so i get height status bar. My problem was solved.
UIView *toViewSnapshot = [toView resizableSnapshotViewFromRect:toView.frame afterScreenUpdates:YES withCapInsets:UIEdgeInsetsZero];

iOS: How to hide status bar after dismissing modal VC that need status bar?

I have a view controller that displays full screen, and from the view controller another modal VC can be presented, the modal VC requires status bar, but after dismissing the modal VC controller, the base VC has the space for status bar on top, and even I set:
[[UIApplication sharedApplication] setStatusBarHidden:YES]
the status bar is hidden but the space is still there, and I checked the frame of the view of the base VC, it starts from 0, I don't think I should make its y-coordinate starts from -20, but what else can I do?
Thanks
Try this out:
[[UIApplication sharedApplication] setStatusBarHidden:NO];
self.view.frame = [[UIScreen mainScreen] applicationFrame];
From the docs on applicationFrame:
This property contains the screen bounds minus the area occupied by
the status bar, if it is visible. Using this property is the
recommended way to retrieve your application’s initial window size.
The rectangle is specified in points.
For a more robust solution, change your frame in response to a status bar frame change. Your application delegate subclass can implement:
-application:willChangeStatusBarFrame:
-application:didChangeStatusBarFrame:
Or, you can register for these notifications using NSNotificationCenter:
UIApplicationWillChangeStatusBarFrameNotification
UIApplicationDidChangeStatusBarFrameNotification
I've tried your method, but it did not work for me, I solved it in this way, when the modal VC dismisses, in the viewDidAppear of base VC I need to:
[[UIApplication sharedApplication] setStatusBarHidden:YES withAnimation:UIStatusBarAnimationNone];
self.wantsFullScreenLayout = YES;
[self.view layoutSubviews];
I know this is not a good solution, but it is the only thing I could think of at the moment, thanks anyway for your help.

How do I get a UINavigationController to NOT change its view size when setting the translucent property?

I have an app where up until now I've been using a UINavigationController with a UINavigationBar that has its property translucent = YES. This means the UINavigationController's content view (i.e. the views from the view controllers you push) to be full-screen (minus status bar).
However, if you set the navigationBar.translucent = NO, this container view becomes 44pt shorter, as I suppose Apple has assumed you don't need any content under an opaque navigationBar.
... except if you're doing what we're doing and are employing a navigationBar that scrolls away (see This Post on how to do that) So I'd like to know if this is possible.
I want to have translucent = NO, but have everything behave as if it were still set to YES. I like the functionality of the translucent = YES, but I don't actually want the bar to be made translucent by UIKit.
What worked for me was to add
extendedLayoutIncludesOpaqueBars = true
in
viewDidLoad
something like this
override func viewDidLoad() {
super.viewDidLoad()
extendedLayoutIncludesOpaqueBars = true
}
Hope it will work for you as well
It's not necessarily a good answer but you could just offset your view that high if you're not translucent.
//This won't take into account orientation and probably other details
if(!self.navigationController.navigationBar.isTranslucent)
{
self.view.frame = CGRectMake(0,0,-44,self.view.bounds.size.height);
}
You could put that in your viewDidLoad or viewWillAppear and if you have a bunch of view controllers you can just subclass them all and put your logic in the subclass.
I found a solution that works, although it is indeed a bit of a hack.
The idea is to give the translucent nav bar an opaque backing. Unfortunately I'm not happy with the solution in that it's dirty and not encapsulated and introduces some potential issues, but i AM happy because it got the job done.
In my Application's base view controller class (i.e. MyViewController : UIViewController), in the viewDidLoad method, I instantiate a new ivar UIView *_navigationBarBG and give it the same frame as self.navigationController.navigationBar. I then set it's backgroundColor property to [UIColor whiteColor] although this is how you achieve some more tint I guess. [EDIT:If you wanted to be a purist (color values remaining exactly as they come from the .psd), you could make the _navigationBarBG a UIImageView and use your custom background there, and the background of the actual UINavigationBar you set to draw clear (or stretch a 1px transparent image if you wanted to use a typical 'change your navigation bar using an image' recipe that's somewhere on the internet)]
if(self.navigationController)
{
_navigationBarBG = [[UIView alloc] initWithFrame: self.navigationController.navigationBar.frame];
_navigationBarBG.backgroundColor = [UIColor whiteColor];
[self.view addSubview:_navigationBarBG];
}
THEN, (and this is the crappy part, but I don't see any other way), I add this view as a subview. BUT, whenever you would normally make a call to [self.view addSubview: anyView], you have to make sure you call [self.view insertSubview: anyView belowSubview: _navigationBarBG];
if (_navigationBarBG)
[self.view insertSubview: anyView belowSubview:_navigationBarBG];
else
[self.view addSubview: anyView];
If you forget that, these added views will slide under your navbar background and look weird. So you need to know that this is a source of error.
WHY AM I DOING THIS? Again you might ask... I want to be able to have a scrolling navigation bar that scrolls out of the way when you scroll down your table view, thereby giving the user more screen space. This is done by using the scrollView delegate (scrollViewDidScroll:) and also viewWillAppear:
// FIRST DEAL WITH SCROLLING NAVIGATION BAR
CALayer *layer = self.navigationController.navigationBar.layer;
CGFloat contentOffsetY = scrollView.contentOffset.y;
CGPoint newPosition;
if (contentOffsetY > _scrollViewContentOffsetYThreshold && self.scrollingNavigationBarEnabled) {
newPosition = CGPointMake(layer.position.x,
22 - MIN((contentOffsetY - _scrollViewContentOffsetYThreshold), 48.0)); // my nav bar BG image is 48.0 tall
layer.position = newPosition;
[_navigationBarBG setCenter: newPosition]; // if it's nil, nothing happens
}
else
{
newPosition = kNavBarDefaultPosition; // i.e. CGPointMake(160, 22) -- portrait only
layer.position = newPosition;
[_navigationBarBG setCenter: newPosition]; // if it's nil, nothing happens
}
I was looking for an answer to this as I wanted my subviews to be at (0,0) and not (0,44)(in reference to the Screen bounds), but I could not find an answer on how to set this in the NavigationController, which I thought would be an included property.
What I ended up doing that was very simple is adding a subview to the navigation controller that was the width and height of the Navigation Bar, but then insert the subview below the Navigation Bar.
Now the setting is Translucent = YES, but it still appears solid and the subviews behave how I want.
EDIT: After re-reading your original post, I suppose if you're going to be rolling the nav bar away, you'll have to take into account hiding and showing the new subview as you do the same with the nav bar

How to cover also the UIStatusBar with UIView when i cover all the screen with the UIView? (iPhone)

How to cover also the UIStatusBar with UIView when i cover all the screen with the UIView?
I was battling how to do this for a long time too! Finally figured it out :) The key is to set the windowLevel of your new window to really high so it lives on top of all the other windows/views/statusbar etc:
UIWindow *keyWin = [UIApplication sharedApplication].keyWindow;
UIWindow *hudWindow = [[UIWindow alloc] initWithFrame:CGRectMake(0.0f, 0.0f, keyWin.frame.size.width, keyWin.frame.size.height)];
hudWindow.backgroundColor = [UIColor blackColor];
hudWindow.alpha = 0.60;
[hudWindow setWindowLevel:10000.0f];
[hudWindow setHidden:NO];
Enjoy!
The best thing you can do is hide the status bar with:
[[UIApplication sharedApplication] setStatusBarHidden:YES animated:NO]
and show it again when you need it.
You can basically removeFromSuperview any time you want to remove the view.
If you're trying to create a UIAlertView-like effect, I don't think you can do this. You might file a feature enhancement request with Apple at http://bugreporter.apple.com.
Nice, but I made these two changes. Adding 1.0 to UIWindowLevel still hides the status bar, and I have no idea why.
self.windowLevel = UIWindowLevelStatusBar+2.0f;
self.userInteractionEnabled = NO;
Setting the Statusbar userInteractionEnabled property to NO will ensure that your scroll views will scroll to top when someone taps on this statusbar.

Resources