Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 984
Jakub Truhlář Avatar asked Jul 13 '15 08:07

Jakub Truhlář


People also ask

What is new with Apple Music?

Apple in June 2021 upgraded its entire music catalog to Lossless Audio with the ALAC (Apple Lossless Audio Codec) that preserves the details in the original audio file. Apple Music subscribers will be able to hear songs exactly as the artists recorded them in the studio.

How much does Apple Music cost in Singapore?

Free 3 months of Apple Music, and $9.98/month thereafter. Listen to 75 million songs data-free on your iOS and Android devices. Free 3 months of Apple Music, and $9.98/month thereafter.

How can I get Apple Music for free?

Pair your eligible audio device to your iPhone or iPad. Open the Apple Music app on your iPhone or iPad and sign in with your Apple ID. If the offer doesn't appear immediately after launching the app, go to the Listen Now tab where it will appear. Tap Get 6 months free.

What is on Apple Music?

Apple Music's library has over 90 million songs. Oh, and you can also watch music videos without ads, and check out Apple's exclusive original content. Apple Music used to have an artist-based social networking feature called Connect. Artists were able to share special content with fans through Connect.


2 Answers

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!

like image 167
Jiri Trecak Avatar answered Sep 19 '22 13:09

Jiri Trecak


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

like image 40
Wes Hager Avatar answered Sep 18 '22 13:09

Wes Hager