The same dynamic status bar as is in the new Apple Music app
Asked Answered
E

4

19

Is it possible to have dynamically coloring statusBar which is in the new Apple Music app ?

enter image description here

Edit:

The new Apple Music app in iOS 8.4 has this feature.

  • Open the app.
  • Select and play a song (status bar is white)
  • Swipe player controller down to see "My music" controller (it has black status bar, maybe you will have to go back in navigation hierarchy).
  • Now just swipe up/down to see dynamic status bar changes.

Edit 2:

Apple documentation does not seem to let us use it right now (iOS 8.4). Will be available probably in the future with iOS 9.

Edit 3: Does not seems to be available in iOS 9 yet.

Edla answered 13/7, 2015 at 8:25 Comment(7)
Thinking about private APISanson
Image: Top white part is the first VC, part with white statusBar and album cover is another VC (above the first VC).Sanson
The issue is that status bar has Dark color in the top half and Light color in the bottom part?Superinduce
No its is NOT an issue. The new Apple Music app in iOS 8.4 has this feature. All you have to do is open the app, select a song (status bar is white) and swipe this controller down to see "My music" controller (it has black status bar). Now just swipe up/down to see dynamic status bar changes.Sanson
Thanks for the updated info. As I know, there no simple solution for this problem. You can look at this project for reference, it uses approach similar to what I suggested you in the answer (But implementing scrollable statusbar behavior). Author takes snapshot of statusbar and adds it as an image to fake statusbar window. github.com/Antondomashnev/UIViewController-ScrollingStatusBarSuperinduce
@AlexanderTkachenko The status Bar is live and functioning on both styles. There's no way that's a snapshot.Chlortetracycline
Snapshot can be done at runtime and image may be updated. I understand it's not one-line of code solution but if it's an important feature of an app, there is a way to do thisSuperinduce
T
4

I am 99.99% sure this cannot be done using public API (easily), because I tried myself almost everything there is (i personally also don't think it is some magical method of their status bar, but instead, their application is able to retrieve status bar view and then just apply mask to it).

What I am sure of is that you can do your own StatusBar and there is MTStatusBarOverlay library for that, unfortunately very old one so I can't really tell if that works but it seems that there are still people who use it.

But using the way library does it, I think there might be solution that sure, requires a lot of work, but is doable, though not "live". In a nutshell you would do this:

  • Take screenshot of top 20 pixels (status bar)
  • From that screenshot, remove everything that is not black (you can improve it so it searches for black edges, this way you can preserve green battery and transparency) This will make your overlay mask and also fake status bar
  • Overlay statusbar with : background view masking actual status bar, and the alpha-image you just created
  • Apply mask to that image, everything that is masked will change color to shades of white
  • Change height of the mask depending on user scroll

Now you should be able to scroll properly and change the color properly. The only problem that it leaves is that status bar is not alive, but is it really? once you scroll out, you immediately remove your overlay, letting it to refresh. You will do the same when you scroll to the very top, but in that case, you change color of the status bar to white (no animation), so it fits your state. It will be not-live only for a brief period of time.

Hope it helps!

Tsar answered 22/7, 2015 at 7:55 Comment(3)
For whoever downvoted this answer, could you please provide your opinion about why it is bad so we can iterate on it? Thank you!Tsar
Rewarded this answer because it offers private api opinion and also provides an example (even if its distressful). True answer is that we all probably have to wait for this.Sanson
They're using private API for the animating in/out table view A-Z index, too, it's definitely not publicly available, and there is precedent in Apple Music for doing private UI things :(Valiancy
S
3

Iterating upon Jiri's answer, this will get you pretty close. Substitute MTStatusBarOverlay with CWStatusBarNotification. To handle the modal transition between view controllers, I'm using MusicPlayerTransition. We're assuming an imageView: "art" in self.view with frame:CGRect(0, 0, self.view.bounds.size.width, self.view.bounds.size.width). Needs a little massaging, but you get the gist. Note: Though we're not "live," the most we'll ever be off is one second, and battery color is not preserved. Also, you'll need to set the animation time in CWStatusBarNotification.m to zero. (notificationAnimationDuration property).

#import "CWStatusBarNotification.h"

#define kStatusTextOffset       5.4  // (rough guess of) space between window's origin.y and status bar label's origin.y

@interface M_Player () <UIGestureRecognizerDelegate> 

@property (retain) UIView *fakeStatusBarView;
@property (retain) CWStatusBarNotification *fakeStatusBar;
@property (retain) UIImageView *statusImgView;
@property (retain) UIImageView *statusImgViewCopy;
@property (retain) UIWindow *window;
@property (strong, nonatomic) NSTimer *statusTimer;

@end

@implementation M_Player
@synthesisze fakeStatusBarView, fakeStatusBar, statusImgView, statusImgViewCopy, window, statusTimer;

-(void)viewDidLoad{

    self.window = [[UIApplication sharedApplication] delegate].window;

    UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleStatusBarDrag:)];
    pan.delegate = self;
    [self.view addGestureRecognizer:pan];
}

-(void)viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];

    if (!fakeStatusBar){
        [self buildFakeStatusBar];
    }

    if (!statusTimer) {
        [self setupStatusBarImageUpdateTimer];
    }

     // optional
    [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleLightContent];
    [self setNeedsStatusBarAppearanceUpdate]; 


-(void)viewDidDisappear:(BOOL)animated{
   [super viewDidDisappear:animated];

   [self destroyStatusBarImageUpdateTimer];

}

-(void)destroyFakeStatusBar{
    [statusImgView removeFromSuperview];
    statusImgView = nil;
    [fakeStatusBarView removeFromSuperview];
    fakeStatusBarView = nil;
    fakeStatusBar = nil;
}

-(void)buildFakeStatusBar{
    UIWindow *statusBarWindow =  [[UIApplication sharedApplication] valueForKey:@"_statusBarWindow"];  // This window is actually still fullscreen. So we need to capture just the top 20 points.

    UIGraphicsBeginImageContext(self.view.bounds.size);
    [statusBarWindow.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    CGRect rect = CGRectMake(0, 0, self.view.bounds.size.width, 20);
    CGImageRef imageRef = CGImageCreateWithImageInRect([viewImage CGImage], rect);
    UIImage *statusImg = [UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);

    statusImg = [statusImg imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];  // This allows us to set the status bar content's color via the imageView's .tintColor property

    statusImgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 20)];
    statusImgView.image = statusImg;
    statusImgView.tintColor = [UIColor colorWithWhite:0.859 alpha:1.000];  // any color you want

    statusImgViewCopy = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 20)];
    statusImgViewCopy.image = statusImg;
    statusImgViewCopy.tintColor = statusImgView.tintColor;


    fakeStatusBarView = nil;
    fakeStatusBar = nil;
    fakeStatusBarView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, 20)];
    [fakeStatusBarView addSubview:statusImgView];

    fakeStatusBar = [CWStatusBarNotification new];
    fakeStatusBar.notificationStyle = CWNotificationStyleStatusBarNotification;
    [fakeStatusBar displayNotificationWithView:fakeStatusBarView forDuration:CGFLOAT_MAX];
}

-(void)handleStatusBarDrag:(UIPanGestureRecognizer*)gestureRecognizer{
    if (gestureRecognizer.state == UIGestureRecognizerStateBegan) {

    }

    if (gestureRecognizer.state == UIGestureRecognizerStateChanged){
        CGPoint convertedPoint = [self.window convertPoint:art.frame.origin fromView:self.view];
        CGFloat originY = convertedPoint.y - kStatusTextOffset;

        if (originY > 0 && originY <= 10) {  // the range of change we're interested in
            //NSLog(@"originY:%f statusImgView.frame:%@", originY, NSStringFromCGRect(statusImgView.frame));

            // render in context from new originY using our untouched copy as reference view
            UIGraphicsBeginImageContext(self.view.bounds.size);
            [statusImgViewCopy.layer renderInContext:UIGraphicsGetCurrentContext()];
            UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            CGRect rect = CGRectMake(0, kStatusTextOffset + originY, self.view.bounds.size.width, 20);
            CGImageRef imageRef = CGImageCreateWithImageInRect([viewImage CGImage], rect);
            UIImage *statusImg = [UIImage imageWithCGImage:imageRef];
            CGImageRelease(imageRef);

            statusImgView.image = statusImg;
            statusImgView.transform = CGAffineTransformMakeTranslation(0, kStatusTextOffset + originY);

        }

       // destroy
        if (originY > 90) {
            [self destroyFakeStatusBar];
        }
    }

    if (gestureRecognizer.state == UIGestureRecognizerStateEnded){

    }
}

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{
    return YES;
}

To keep your status bar screenshots in sync with the actual status bar, setup your timer. Fire it in viewWillAppear, and kill it in viewDidDisappear.

-(void)setupStatusBarImageUpdateTimer{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_async(dispatch_get_main_queue(), ^(){
            // main thread
            if (!statusTimer) {
                statusTimer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(handleStatusTimer:) userInfo:nil repeats:YES];
                [[NSRunLoop currentRunLoop] addTimer:statusTimer forMode:NSRunLoopCommonModes];
            }
        });
    });
}

-(void)destroyStatusBarImageUpdateTimer{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_async(dispatch_get_main_queue(), ^(){
            // main thread
            [statusTimer invalidate];
            statusTimer = nil;
        });
    });
}

-(void)handleStatusTimer:(NSTimer*)timer{
    UIWindow *statusBarWindow =  [[UIApplication sharedApplication] valueForKey:@"_statusBarWindow"];

    UIGraphicsBeginImageContext(CGSizeMake(self.view.bounds.size.width, 20));
    [statusBarWindow.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage *viewImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    CGRect rect = CGRectMake(0, 0, self.view.bounds.size.width, 20);
    CGImageRef imageRef = CGImageCreateWithImageInRect([viewImage CGImage], rect);
    UIImage *statusImg = [UIImage imageWithCGImage:imageRef];
    CGImageRelease(imageRef);

    statusImg = [statusImg imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
    statusImgViewCopy.image = statusImg;
}

Because we have a strong reference to the timer and setup and invalidation happens on the same thread, there's no worrying about the timer failing to invalidate. The final result should look something like this:

enter image description here

Sloatman answered 11/3, 2016 at 20:22 Comment(0)
C
0

At first glance it looked like a manipulation of a snapshot from the status bar but the status bar is live on both ends so that's not the case.

At second glance it looked like some new api that was introduced in iOS 8.4 but after reviewing the api I couldn't find anything related to that.

It seems very odd to me that apple would use private apis in her own app. This would results some really bad example for developers but then again, there is nothing public that will let you have two styles on your live statusbar.

This leaves us with private api or black magic.

Chlortetracycline answered 19/7, 2015 at 11:6 Comment(2)
Why would it be weird if Apple used private API's?Inez
I'm sure apple is using a lot of private stuff behind the stage but for something visual that developers clearly can't use.. feels a bit off to me.Chlortetracycline
S
-1

Thinking about how to implement this without private APIs.

I think there may be a solution with second UIWindow overlaying your statusBar.

Adding view on StatusBar in iPhone

Maybe it's possible to make screenshots of status bar constantly (taken from your main Window) to the image, apply some filter on it and display this 'fake statusbar image' on your second window (above 'real' statusBar).

And you can do what you want with the second "fake" statusbar.

Superinduce answered 15/7, 2015 at 10:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.