Customize the Airplay button's appearance

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


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.


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: