I am developing an iPhone application that supports AirPlay using MPMoviePlayerController. But, I need to display this AirPlay button in my custom-view. So, I took MPVolumeView and added it to my custom view; removed all the subviews from MPVolumeView except AirPlay button.
The problem is:
Can I change the frame of Volume view so that it fits in corner of my custom view with the size of AirPlay button? I know it is possible to handle this programmatically; Is it valid to do like this? The link Customize the Airplay button's appearance mentions that we should not change shape, position of AirPlay button.
I need to set the customized image to AirPlay button so that it matches my Custom View aesthetically. How do I do this?
Whenever device with AirPlay is not present, AirPlay button just disappears from MPVolumeView. Is there any notification available when button disappears? I need to adjust my custom view when AirPlay button is not present. Is there any method to identify if AirPlay button is present or not? The MPVolumeView subviews array has this button and it is not in hidden state even if it is not displayed in MPVolumeView.
You should take a look at this answer: Customize the Airplay button's appearance
It's basically the same as my answer, but with more detail. I think it answers most of your question.
Pasting it here, for your convenience:
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 have done it more simpler by adding the MPVolumeView to UIBarButtonItem
- (void) initAirPlayPicker {
MPVolumeView *volumeView = [[MPVolumeView alloc] initWithFrame:CGRectMake(0,0,44,44)];
volumeView.tintColor = [UIColor blueColor];
volumeView.backgroundColor = [UIColor clearColor];
UIImage *imgAirPlay = nil;
for( UIView *wnd in volumeView.subviews ) {
if( [wnd isKindOfClass:[UIButton class] ]) {
self.airPlayWindow = (UIButton*) wnd;
UIImage *img = _airPlayWindow.currentImage;
imgAirPlay = [img imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
[volumeView setRouteButtonImage: imgAirPlay forState:UIControlStateNormal];
break;
}
}
volumeView.showsRouteButton = YES;
volumeView.showsVolumeSlider = NO;
[volumeView sizeToFit];
self.airPlayButton = [[UIBarButtonItem alloc] initWithCustomView:volumeView];
}
By adding the MPVolumeView to the view hierarchy its gets the notification about the Airplay state and is workling properly.
Related
Hi i am making customUIButtons in my app by using the following piece of code.
+ (NSArray *) createButtonItemNormalImage:(UIImage *)normalImage
highlightImage:(UIImage *)highlightImage
disabledImage:(UIImage *)disabledImage
touchUpSelector:(SEL)selector
target:(id)target
{
// HighlightImage is not used. Highlight is shown using iOS glow
UIButton *uiButton = [UIButton buttonWithType:UIButtonTypeCustom];
uiButton.bounds = CGRectMake(0,
0,
normalImage.size.width,
normalImage.size.height);
[uiButton setImage:normalImage
forState:UIControlStateNormal];
if (disabledImage)
{
[uiButton setImage:disabledImage
forState:UIControlStateDisabled];
}
[uiButton addTarget:target
action:selector
forControlEvents:UIControlEventTouchUpInside];
uiButton.showsTouchWhenHighlighted = YES;
UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithCustomView:uiButton];
return [NSArray arrayWithObjects:buttonItem, uiButton, nil];
}
I have made a cancel button using the above function. The cancel button takes the user from one screen to another screen. The problem is when i come back to the first screen the cancel button is still glowing. I have seen this problem before also but a call to [self.view setNeedsLayout] used to solve it.
Why does it happen and what would be a correct way of solving it?
Thanks!
To solve this problem in not so standard way I now set the highlighted state to no for all the buttons when I enter the first screen. I use myButton.highlighted = NO;. However the documentation says following for highlighted property.
Specify YES if the control is highlighted; otherwise NO. By default, a control is not highlighted. UIControl automatically sets and clears this state automatically when a touch enters and exits during tracking and when there is a touch up.
Its not happening so in my case.I would love to know the reason behind it and standard ways of solving it
In my app, I need to parse some data from the network, and add some customized buttons.
Later, when user click on it, I would provide more details.
An image view is the background for the app
The position of these buttons(xPos, yPos) are parsed from the server(dynamic data)
no prints when I click on these buttons that I add programmatically
The code I have for adding it is like this
...
[businessButton setImage:businessImage forState:UIControlStateNormal];
[businessButton setFrame:CGRectMake([xPos floatValue], [yPos floatValue], [businessImage size].width/2, [businessImage size].width/2)];
[businessButton addTarget:self.imageView action:#selector(serviceProviderSelected:) forControlEvents:UIControlEventTouchUpInside];
...
- (void)serviceProviderSelected:(id)sender
{
NSLog(#"sp tapped\n");
}
I created another dummy app to do (what I think is the same thing), and the button works out fine...
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
UIButton *customizedButton = [UIButton buttonWithType:UIButtonTypeCustom];
UIImage *image = [UIImage imageNamed:#"business-icon.png"];
[customizedButton setImage:image forState:UIControlStateNormal];
[customizedButton setFrame:CGRectMake(100, 100, 20, 20)];
[customizedButton addTarget:self action:#selector(customButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:customizedButton];
}
- (IBAction)buttonPressed:(id)sender {
NSLog(#"yes, pressed\n");
}
I've been suspecting that the buttons I create in code are already released, and printed out the array that I use for storing these Buttons, they are valid addresses though...
Thanks!
William
I just found out why.
Instead of [self.imageView addSubview:businessButton];, [self.view addSubview:businessButton]; works now.
I think I didn't really understand how the view relationship was after adding an imageView in storyboard.
UIImageView has user interactions disabled by default. you have to set this property to YES in order to get an UIButton or anything else working as it's subview
You need to do:
[businessButton addTarget:self action:#selector(serviceProviderSelected:) forControlEvents:UIControlEventTouchUpInside];
The target needs to be the class that defines the selector.
I have an error alert view that crash when I click "OK"
- (id)initWithFrame:(CGRect)frame {
if ((self = [super initWithFrame:frame])) {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setTitle:#"OK" forState:UIControlStateNormal];
[button setFrame:CGRectMake(10, 80, 266, 43)];
[button addTarget:self action:#selector(dismissError:) forControlEvents:UIControlEventTouchUpInside];
[alertView addSubview:button]; //This is a subview of
//...other stylings for the custom error alert view
errorWindow = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
errorWindow.windowLevel = UIWindowLevelStatusBar;
errorWindow.hidden = NO;
[errorWindow addSubview:alertView];
[errorWindow makeKeyAndVisible];
}
This alertView is inside the custom ErrorAlert:UIView.
This alert shows up fine.
However, when click on "OK" button, app crashes, it never reached
- (void)dismissError:(id)sender;
Am I adding the button at the wrong place? (It gives the generic int retVal=......EXC_BAD_ACCESS)
You should not create a new UIWindow unless you're showing something on an external display.
In iOS, windows do not have title bars, close boxes, or any other visual adornments. A window is always just a blank container for one or more views. Also, applications do not change their content by showing new windows. When you want to change the displayed content, you change the frontmost views of your window instead.
Take a look at View Programming Guide for iOS
I have a method makeButtons (posted here), which is removing all buttons in the screen and adding them again. This works fine in viewDidLoad and viewDidAppear. I am accessing information from a webservice which is telling me I need a new button. When I am calling [self makebuttons] from that method, nothing happends, until I move forth and back with my NavigationController forcing viewDidAppear to do the work again. My question is why? I am doing exactly the same, unless it's not called from viewDidAppear, but from doneGettingInformation.
- (void) viewDidAppear:(bool) animated {
[self makebuttons]; // Works great!
}
- (void) doneGettingInformation : (ASIFormDataRequest *) request {
NSString *response = [request responseString];
[[self.temp.userInfo objectForKey:#"spillider"] addObject:response];
[self makebuttons]; // This gets called, but nothing changes in the view itself.
}
- (void) makeButtons {
NSLog(#"kjort");
int newAntall = [[self.temp.userInfo objectForKey:#"spillider"] count];
for (UIButton * button in gameButtons) {
NSString *tag = [NSString stringWithFormat:#"%i",button.tag];
[button removeFromSuperview];
if ([webviews objectForKey:tag]) {
[[webviews objectForKey:tag] removeFromSuperview];
[webviews removeObjectForKey:tag];
}
}
[gameButtons removeAllObjects];
scroller.contentSize = CGSizeMake(320, 480);
if (newAntall > 3) {
CGSize scrollContent = self.scroller.contentSize;
scrollContent.height = scrollContent.height+((newAntall-3)*BUTTON_HEIGTH);
self.scroller.contentSize = scrollContent;
}
int y = 163;
self.nyttSpillKnapp.frame = CGRectMake(BUTTON_X, y, BUTTON_WIDTH, 65);
for (int i=0; i<newAntall; i++) {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setBackgroundImage:[UIImage imageNamed:#"knapp_midt"] forState:UIControlStateNormal];
[button setBackgroundImage:[UIImage imageNamed:#"knapp_midt"] forState:UIControlStateHighlighted];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
[button.titleLabel setFont:[UIFont systemFontOfSize:15]];
button.frame = CGRectMake(BUTTON_X, y, BUTTON_WIDTH, BUTTON_HEIGTH);
button.enabled = YES;
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc] initWithTarget:self
action:#selector(deleteButton:)];
swipe.direction = UISwipeGestureRecognizerDirectionRight;
[button addGestureRecognizer:swipe];
button.tag = [[[self.temp.userInfo objectForKey:#"spillider"] objectAtIndex:i] intValue];
NSString * tittel = [NSString stringWithFormat:#"spill %#",[[self.temp.userInfo objectForKey:#"spillider"] objectAtIndex:i]];
[button setTitle:tittel forState:UIControlStateNormal];
UIButton *subButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
subButton.transform = CGAffineTransformMakeRotation(M_PI_2);
subButton.tag = i;
CGRect subframe = CGRectMake(230, 5, subButton.frame.size.width, subButton.frame.size.height);
subButton.frame = subframe;
CGRect myframe = self.nyttSpillKnapp.frame;
myframe.origin.y = myframe.origin.y+BUTTON_HEIGTH;
self.nyttSpillKnapp.frame = myframe;
[subButton addTarget:self action:#selector(clickGameButton:) forControlEvents:UIControlEventTouchUpInside];
[button addSubview:subButton];
[gameButtons addObject:button];
[self.scroller addSubview:button];
y += BUTTON_HEIGTH;
}
}
To sum up, it only works if I am changing viewcontrollers back and forth causing viewWillAppear to get called. Why is that?
I am sorry for my messy methods.
Thanks
If you change the contents of the view outside of the initial view appearing process or layout changes, it's your responsibility to call setNeedsDisplay and inform the run loop that it needs to be redrawn.
The system will ask the view to draw it's contents initially or during layout changes which is why it works as part of the process to first show the view. During that initial process, the viewWill/DidAppear delegates will get called.
From the UIView class reference:
The View Drawing Cycle
View drawing occurs on an as-needed basis. When a view is first shown,
or when all or part of it becomes visible due to layout changes, the
system asks the view to draw its contents. For views that contain
custom content using UIKit or Core Graphics, the system calls the
view’s drawRect: method. Your implementation of this method is
responsible for drawing the view’s content into the current graphics
context, which is set up by the system automatically prior to calling
this method. This creates a static visual representation of your
view’s content that can then be displayed on the screen.
When the actual content of your view changes, it is your
responsibility to notify the system that your view needs to be
redrawn. You do this by calling your view’s setNeedsDisplay or
setNeedsDisplayInRect: method of the view. These methods let the
system know that it should update the view during the next drawing
cycle. Because it waits until the next drawing cycle to update the
view, you can call these methods on multiple views to update them at
the same time.
EDIT:
Also, make sure done getting images is not called on a background thread. You can't edit views on a background thread. If it is you can prepare all the data on a bg thread but then call makeButtons on on the main thread (performSelectorOnMainThread or use blocks.
See GCD, Threads, Program Flow and UI Updating
I am currently playing audio through AudioQueues. I would like to allow users to connect to Airplay devices.
If I create an MPVolumeView and use the 'showsRouteButton' to display the route button I can successfully connect.
Is there a way to change the Audio Route to Airplay without using the MPVolumeView? Or a simpler Apple view that is just the route button?
1 hide MPVolumeView and make it as global var
CGRect frame = CGRectZero;
frame.origin.y = 0;
frame.origin.x = 410; // out of the screen
_volumeView = [[MPVolumeView alloc] initWithFrame:frame];
[_volumeView setShowsVolumeSlider:NO];
[_volumeView setShowsRouteButton:YES];
[self.view addSubview:_volumeView];
2 simulate button tapes
- (IBAction)handleAirPlay:(id)sender {
for (UIButton *button in _volumeView.subviews)
{
if ([button isKindOfClass:[UIButton class]])
{
[button sendActionsForControlEvents:UIControlEventTouchUpInside];
}
}
}
I don't think there is any other way to show the airplay route button (at least in current SDK iOS 5.1).. If you want to show AirPlay options you have to use MPVolumeView..
Since iOS 11 you can use AVRoutePicker :
import AVKit
let rpv = AVRoutePickerView()
view.addSubview(rpv)