18

I use a standard gradient overlay (done in photoshop) to make buttons look nicer in my app. I added an Airplay button, but the aesthetics are not matching.

enter image description here

I really want to put a gradient layer over it so it matches, but anything I can find only shows how to do this with a png, not an existing UIView. If not the gradient layer, I just need someway, any way, to change the appearance of the Apple airplay button while keeping its functionality intact.

The setup code is simple:

MPVolumeView *volumeView = [[MPVolumeView alloc] initWithFrame:frame];
[volumeView setShowsVolumeSlider:NO];
[bottomPanel addSubview:volumeView];

How can I get the appearance of this to match my controls?

coneybeare
  • 33,113
  • 21
  • 131
  • 183
  • If others read this question after it's been answered: **don't ever change the glyph/shape and position!!** Thank you :) –  Mar 02 '11 at 22:16
  • In other words, "how do I make an active button appear disabled?" I'd suggest you not confuse the user instead and make your images white. – Nicholas Riley Mar 02 '11 at 22:22
  • The whole UI is themed in this unobtrusive style, and the app UI has been highly praised. Thanks for your opinion though. – coneybeare Mar 02 '11 at 22:26
  • "The route button is visible by default when there is more than one audio output route available." I only have one audio output, so I can't even get the button to show. Is there anyway to simulate multiple audio outputs so that I can play around with this button? – Erik B Mar 25 '11 at 14:49
  • @coneybeare If I could just get the button to appear I would make a real effort to change its appearance. – Erik B Mar 25 '11 at 15:50
  • @Erik B: http://itunes.apple.com/us/app/airview/id412370918 – coneybeare Mar 25 '11 at 16:32
  • @coneybeare, Thank you. I found a bluetooth headset before I saw your link. I hope you see my answer before the bounty expires. – Erik B Mar 26 '11 at 18:54

5 Answers5

16

I finally found a bluetooth headset so that I could test the button. Changing its appearance was very simple. Here's the code:

for (UIButton *button in volumeView.subviews) {
    if ([button isKindOfClass:[UIButton class]]) {
        [button setImage:[UIImage imageNamed:@"custom-route-button.png"] forState:UIControlStateNormal];
        [button sizeToFit];
    }
}

That's all there's to it.

Erik B
  • 40,889
  • 25
  • 119
  • 135
  • 1
    +1 for correct answer - still, I would like to add that this way of manipulating the standard interface and functionality of apple's components is discouraged and MAY be a reason for getting rejected within the approval process. Additionally, as soon as apple decides to add additional buttons or change their order, this will fail. Too bad that there is no other way to change the appearance of MPVolumeView and its subviews/controls. – Till Mar 26 '11 at 19:16
  • 2
    @Till True, but it's quite unlikely that Apple would just add another button, because that would break any UI. Anyway, it's up to the OP to assess the risks. I just provide the answer. – Erik B Mar 26 '11 at 20:04
  • I was hoping for an answer that would not require this, but sometimes you gotta do what you gotta do. – coneybeare Mar 26 '11 at 23:03
  • Does anybody know if Apple is approving apps that slightly modify the appearance of the AirPlay button? – bdmontz May 31 '11 at 16:33
  • @bdmontz I don't have any apps on the app store with a modified airplay button, so I can't say that I know, but this solution doesn't use private API methods, so unless Apple has a specific policy against modifying the airplay button I think you'll be fine. Besides, creating the customized button takes less than 5 minutes and restoring the default look is even quicker. It will certainly be more time consuming to read the guidelines to figure out if Apple seems to allow it and even then you won't know for sure. – Erik B May 31 '11 at 17:00
  • 2
    Ambiance http://ambianceapp.com is a high profile app that this has been used in. There have been no issues with it at all – coneybeare Jun 03 '11 at 22:07
  • 4
    How do you get notified if the AirPlay button is added later on (with an automatic animation?) – steipete Aug 30 '11 at 21:18
  • @ErikB have you tried this with iOS5? I can't seem to get any change with the PNG's I add to the button. – mahboudz Oct 26 '11 at 20:26
  • @mahboudz, No, I haven't. I think you're better of asking coneybeare, who is the one that's using this code in an app. – Erik B Oct 27 '11 at 19:30
  • @coneybeare have you tried this with iOS5? I can't seem to get any change with the PNG's I add to the button. – mahboudz Oct 28 '11 at 01:08
  • Even though I accepted this answer, I never used it but solved it a slightly different way using KVO. I will post my answer here – coneybeare Oct 28 '11 at 12:09
16

After accepting @Erik B's answer and awarding the bounty to him, I found that there was more tweaking necessary to get it to work. I am posting here for the benefit of future SO searchers.

The problem I was seeing was that the internal mechanisms of the buttons would assign the image based on the current airplay state. Thus any customizations I made during init would not stick if the Airplay receiver went away, or the state was changed somehow. To solve this, I setup a KVO observation on the button's alpha key. I noticed that the button is always faded in/out which is an animation on alpha.

MPVolumeView *volumeView = [[MPVolumeView alloc] initWithFrame:CGRectZero];
[volumeView setShowsVolumeSlider:NO];
for (UIButton *button in volumeView.subviews) {
    if ([button isKindOfClass:[UIButton class]]) {
        self.airplayButton = button; // @property retain
        [self.airplayButton setImage:[UIImage imageNamed:@"airplay.png"] forState:UIControlStateNormal];
        [self.airplayButton setBounds:CGRectMake(0, 0, kDefaultIconSize, kDefaultIconSize)];
        [self.airplayButton addObserver:self forKeyPath:@"alpha" options:NSKeyValueObservingOptionNew context:nil];
    }
}
[volumeView sizeToFit];

Then I observe the changed value of the buttons alpha.

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([object isKindOfClass:[UIButton class]] && [[change valueForKey:NSKeyValueChangeNewKey] intValue] == 1) {
        [(UIButton *)object setImage:[UIImage imageNamed:@"airplay.png"] forState:UIControlStateNormal];
        [(UIButton *)object setBounds:CGRectMake(0, 0, kDefaultIconSize, kDefaultIconSize)];
    }
}

Don't forget to remove the observer if you destroy the button

- (void)dealloc {
    [self.airplayButton removeObserver:self forKeyPath:@"alpha"];
    …
}

Based on code observation, the button will break if Apple changes the internal view hierarchy of the MPVolumeView to add/remove/alter the views such that a different button comes up. This makes it kind of fragile, so use at your own risk, or come up with a plan b in case this happens. I have been using it for over a year in production with no issues. If you want to see it in action, check out the main player screen in Ambiance

coneybeare
  • 33,113
  • 21
  • 131
  • 183
  • It appears to be still working in iOS 5, but triggering the action when no AirPlay devices are available triggers an empty Alert View without any buttons, basically crashing your app. – THM Mar 04 '12 at 20:23
  • OK, got it to work. You need to actually insert the dummy volumeView somewhere in the view hierarchy, or it won't show the Alert Sheet. – THM Mar 04 '12 at 21:29
  • 1
    How can you tell if it is the selected (blue) or unselected (white) button that is currently visible? I know its possible, Spotify does it. :-) – Dermot Jun 11 '12 at 01:36
  • 2
    In my iPhone app, i'm adding MPVolumeView, but why am I not able to see the "airplay" button along with the volume slider? – Satyam Aug 04 '12 at 10:14
  • You don't need KVO, tested on iOS7. Initial setup is enough. If you need both states of button to be changed then provide image for UIControlStateNormal and UIControlStateSelected. Good luck – iWheelBuy Mar 31 '14 at 16:45
  • KVO is necessary for when airplay sources appear and disappear, as the button sometimes reverts. – coneybeare Apr 01 '14 at 00:19
7

Update: As of iOS 13, this method is deprecated!

I think there's a better solution to this since iOS 6: Use

- (void)setRouteButtonImage:(UIImage *)image forState:(UIControlState)state;

on MPVolumeView.

See https://developer.apple.com/Library/ios/documentation/MediaPlayer/Reference/MPVolumeView_Class/index.html#//apple_ref/occ/instm/MPVolumeView/setRouteButtonImage:forState:

Micky
  • 5,578
  • 7
  • 31
  • 55
3

The above code didn't work for me (as the AirPlay button is added later). As a workaround you can a standard UIButton in your UI and trigger an AirPlay in a (hidden) MPVolumeView button with

for (UIButton *button in volumeView.subviews)
{
    if ([button isKindOfClass:[UIButton class]]) 
    {
        [button sendActionsForControlEvents:UIControlEventTouchUpInside];
    }
}

A side effect is that the button doesn't auto hide when only one route is available, which may or may not be desired behavior.

voidStern
  • 3,678
  • 1
  • 29
  • 32
  • This seems to be dangerous to use, as iOS 5 will pop up an empty alert view without any controls to dismiss it, when there are no AirPlay devices available. – THM Mar 04 '12 at 20:24
  • Sorry, but I can't reproduce that. For me it brings an alert view with iPhone and Cancel as options. (iOS 5.1) – voidStern Mar 05 '12 at 18:33
  • 2
    I think I got it: this may happen when the MPVolumeView isn't anywhere in the view hierarchy. I think maybe it needs some parent frame to show the Action Sheet from. Anyway: By adding the MPVolumeView to the hierarchy and hiding it, your solution still works. – THM Mar 05 '12 at 23:23
1

I created a custom method which works perfectly...

-(void)airplayIcon:(CGRect)rect {
    UIView *aiplayView = [[UIView alloc] initWithFrame:self.bounds];
    aiplayView.clipsToBounds = true;
    aiplayView.backgroundColor = [UIColor clearColor];
    [self addSubview:aiplayView];

    MPVolumeView *airplayVolume = [[MPVolumeView alloc] initWithFrame:aiplayView.bounds];
    airplayVolume.showsVolumeSlider = false;
    [aiplayView addSubview:airplayVolume];

    for (UIButton *button in airplayVolume.subviews) {
        [button setFrame:self.bounds];
        if ([button isKindOfClass:[UIButton class]]) {
            [button setImage:[UIImage imageNamed:@"normal.png"] forState:UIControlStateNormal];
            [button setImage:[UIImage imageNamed:@"selected.png"] forState:UIControlStateSelected];
            [button sizeToFit];

        }

    }

}
Joe Barbour
  • 842
  • 9
  • 14