MPVolumeView doesn't redraw correctly in iOS 7 - ios

So I have an iOS7 app that I'm using a MPVolumeView in so the user can control the volume level. I have the route button hidden on this particular MPVolumeView and use another MPVolumeView with the slider disabled as my AirPlay icon.
My app supports both Portrait and landscape orientations, and the volume slider is a different width in these two different modes.
If the view gets initialized in Landscape mode first, then the MPVolumeView will resize itself correctly.
However, when the view gets initialized in Portrait mode and then I rotate to Landscape mode, EVERYTHING else in the app gets resized/moved except the MPVolumeView only gets moved, and it doesn't get shorter like it should.
I am using custom images on the MPVolumeView, and if I remove the custom images for the track, this problem goes away.
Here is the code used to initialize the MPVolumeView
self.volumeView = [[MPVolumeView alloc] initWithFrame:CGRectZero];
self.volumeView.showsRouteButton = NO;
self.volumeView.layer.borderColor = [[UIColor redColor] CGColor];
self.volumeView.layer.borderWidth = 1.0f;
if (AT_LEAST_IOS7) {
// TODO: BUGBUG iOS7 doesn't redraw this MPVolumeView correctly when it's frame changes (i.e. we rotate to the portrait view).
// These images simply make the bar a little thicker than the standard thickness (to match the iOS7 music app) but it's not redrawing
// correctly so we are going to have to live with a slightly thinner bar.
[self.volumeView setMaximumVolumeSliderImage:[UIImage imageNamed:#"volume_bar_max"] forState:UIControlStateNormal];
[self.volumeView setMinimumVolumeSliderImage:[UIImage imageNamed:#"volume_bar_min"] forState:UIControlStateNormal];
[self.volumeView setVolumeThumbImage:[UIImage imageNamed:#"volume_scrubber"] forState:UIControlStateNormal];
}
[self addSubview:self.volumeView];
And in layoutSubviews I am repositioning/scaling it's frame:
self.volumeView.frame = CGRectIntegral(CGRectMake(controlsLeft + kEdgeToSliderSideWidth,
volumeTop,
controlsWidth - (2 * kEdgeToSliderSideWidth),
volumeSize.height));
Here is what it looks like when the view starts out in Portrait Mode: (overall width of 640 pixels)
And when it gets rotated to Landscape it ends up looking like this: (overall width of 568 pixels)
Does anybody have any ideas?

I'm experiencing the same issue. My current workaround is to destroy and re-add the slider after a screen rotation:
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
//remove the old view
for (UIView *subview in self.volumeViewContainer.subviews)
[subview removeFromSuperview];
//recreate it
UIImage *slider = [[UIImage imageNamed:#"slider"] resizableImageWithCapInsets:UIEdgeInsetsMake(0.0, 15.0, 0.0, 15.0)];
UIImage *knobImage = [UIImage imageNamed:#"slider_knob"];
MPVolumeView *volumeView = [[[MPVolumeView alloc] initWithFrame:self.volumeViewContainer.bounds] autorelease];
[volumeView setMinimumVolumeSliderImage:slider forState:UIControlStateNormal];
[volumeView setMaximumVolumeSliderImage:slider forState:UIControlStateNormal];
[volumeView setVolumeThumbImage:knobImage forState:UIControlStateNormal];
[self.volumeViewContainer addSubview:volumeView];
}
Doing that there is still the issue that the slider is still displayed with glitches DURING rotation. That's why I render the slider to an image and swap the slider with said image before a rotation takes place:
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
//render to UIImage
UIGraphicsBeginImageContextWithOptions(self.volumeViewContainer.frame.size, NO, 0.0);
[self.volumeViewContainer.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
//remove old slider
for (UIView *subview in self.volumeViewContainer.subviews)
[subview removeFromSuperview];
//add UIImageView which resizes nicely with the container view
UIImageView *imageView = [[[UIImageView alloc] initWithFrame:self.volumeViewContainer.bounds] autorelease];
imageView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
imageView.image = img;
[self.volumeViewContainer addSubview:imageView];
}
I know that this is not a real solution, but I think it's better than nothing and I hope it helps you.

After doing a lot of experimentation, it seems that MPVolumeView is very buggy in iOS 7.0.x.
I was having an issue where having custom images for min/max sliders, volume thumb and route would sometimes cause the slider to expand to cover the route button.
When I changed the order in which these are invoked, it fixed the issue. The trick is to call setVolumeThumbImage and setRouteButtonImage first, then setMinimumVolumeSliderImage and setMaximumVolumeSliderImage.
That took care of the overlapping track over the route button.
HOWEVER, this caused a new issue, wherein the maximumSliderImage began overlapping the volumeThumbImage!
The final solution was to bring the thumb layer to front as follows, in a subclass of MPVolumeView:
- (void)_initialize {
UIImage *scrubber = [UIImage imageNamed:#"volume_scrubber_grip"];
scrubberSize = scrubber.size;
[self setVolumeThumbImage:scrubber forState:UIControlStateNormal];
[self setRouteButtonImage:[UIImage imageNamed:#"airplay_button"] forState:UIControlStateNormal];
[self setMinimumVolumeSliderImage:[UIImage imageNamed:#"min_slider"] forState:UIControlStateNormal];
[self setMaximumVolumeSliderImage:[UIImage imageNamed:#"max_slider"] forState:UIControlStateNormal];
}
- (void)layoutSubviews {
[super layoutSubviews];
[self.subviews enumerateObjectsUsingBlock:^(UIView *obj, NSUInteger idx, BOOL *stop) {
if([obj isKindOfClass:[UISlider class]]) {
UISlider *slider = (UISlider *)obj;
[slider.subviews enumerateObjectsUsingBlock:^(UIView *obj2, NSUInteger idx, BOOL *stop2) {
if(CGSizeEqualToSize(obj2.frame.size, scrubberSize)) {
[obj bringSubviewToFront:obj2];
*stop2 = YES;
}
}];
*stop = YES;
}
}];
}

I tested another solution that works fine. I embed the MPVolumeView in a simple UIView (that can be created in the storyboard). Then I do key-value observing on the property "bounds" of the UIView. When this property changes, I update MPVolumeView's frame to the new bounds of the UIView.
This solution handles both, autorotation and autolayout.
import UIKit
import MediaPlayerclass ViewController: UIViewController {
#IBOutlet weak var volumeViewContainer: UIView!
override func viewDidLoad() {
super.viewDidLoad()
volumeViewContainer.backgroundColor = UIColor.grayColor()
var volumeView = MPVolumeView(frame: volumeViewContainer.bounds)
volumeViewContainer.addSubview(volumeView)
volumeViewContainer.addObserver(self, forKeyPath: "bounds", options: nil, context: nil)
}
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if let subview = volumeViewContainer.subviews.first as? UIView {
subview.frame = volumeViewContainer.bounds
}
}
}

#IBOutlet var volumeViewHolder: UIView!
override func viewDidLoad() {
super.viewDidLoad()
volumeViewHolder.backgroundColor = UIColor.clearColor()
var volumeView : MPVolumeView = MPVolumeView()
volumeView.frame = volumeViewHolder.frame
volumeView.center = volumeViewHolder.center
volumeView.sizeToFit()
volumeView.showsRouteButton = false
volumeView.showsVolumeSlider = true
volumeView.autoresizingMask = UIViewAutoresizing.FlexibleWidth | UIViewAutoresizing.FlexibleHeight
volumeViewHolder.addSubview(volumeView)
}

Related

MPVolumeView animation issue

Every time I add MPVolumeView as a subview to my UIViewController’s view, there is a quick animation (the MPVolumeView expands from left to right) which looks really weird. I’m looking for a way to get rid of this animation, has anyone faced this issue?
I almost accepted that this is a MPVolumeView bug but then I noticed that Apple is definitely using a MPVolumeView in their native music app, no weird animations there...
So there must be something I'm doing wrong.
UPDATE:
The code is pretty straightforward but it was requested in the comments, so here it is:
MPVolumeView *volumeView = [[MPVolumeView alloc] initWithFrame:CGRectMake(10.f, 0.f, CGRectGetWidth(self.view.frame) - 20.f, 30.f)];
[[UISlider appearanceWhenContainedIn:[MPVolumeView class], nil] setMinimumValueImage:[UIImage imageNamed:#"icon-volumeMin"]];
[[UISlider appearanceWhenContainedIn:[MPVolumeView class], nil] setMaximumValueImage:[UIImage imageNamed:#"icon-volumeMax"]];
volumeView.center = CGPointMake(0.5f * CGRectGetWidth(self.view.frame), 0.5f * CGRectGetHeight(self.view.frame));
volumeView.showsRouteButton = NO;
[self.view addSubview:volumeView];
I made a very simple project on github to demostrate the problem, but you have to run it on a device, since MPVolumeView does not show up on simulator. Or just take a look at this gif:
:
One possible way to remove this behavior is to subclass MPVolumeView and perform some additional work after [super layoutSubviews].
- (void)layoutSubviews
{
[super layoutSubviews];
[self xy_recursiveRemoveAnimationsOnView:self];
}
- (void)xy_recursiveRemoveAnimationsOnView:(UIView *)view
{
[view.layer removeAllAnimations];
for (UIView *subview in view.subviews) {
[self xy_recursiveRemoveAnimationsOnView:subview];
}
}
This removes all inserted animations. So be sure that is what you want, since this is quite the overkill. One could also just remove the position and bounds animations (see removeAnimationForKey:).
i fixed your demo-code
#implementation F17YViewController {
MPVolumeView *volumeView;
}
- (void)viewDidLoad {
[super viewDidLoad];
volumeView = [[MPVolumeView alloc] init];
volumeView.showsRouteButton = NO;
volumeView.hidden = true;
[[UISlider appearanceWhenContainedIn:[MPVolumeView class], nil] setMinimumValueImage:[UIImage imageNamed:#"icon-volumeMin"]];
[[UISlider appearanceWhenContainedIn:[MPVolumeView class], nil] setMaximumValueImage:[UIImage imageNamed:#"icon-volumeMax"]];
[self.view addSubview:volumeView];
}
- (void)viewWillLayoutSubviews {
[super viewWillLayoutSubviews];
volumeView.frame = CGRectMake(10.f, 0.f, CGRectGetWidth(self.view.frame) - 20.f, 30.f);
volumeView.center = CGPointMake(0.5f * CGRectGetWidth(self.view.frame), 0.5f * CGRectGetHeight(self.view.frame));
}
- (IBAction)showVolumeView:(id)sender {
volumeView.hidden = false;
}
You should do layout calls in viewWillLayoutSubviews.
Instead of creating a new MPVolumeView with each button press you should create one at viewDidLoad and hide it and then unhide when the button gets pressed.

self.window.rootViewController.view addSubview: doesn't load subview

I have a UITableView with custom cells in it. Each cell has 4 UILabels and 3 UIImageViews, all with user interaction disable.
At didSelectRowAtIndexPath, another view controller (with transparent background) is loaded and animated added to the root view. Simulating a pop up:
UIView *rootView = UIApplication.sharedApplication.delegate.window.rootViewController.view;
[rootView addSubview:view];
When the app is first installed in the device (built for debug or release), the subview is not seen when a UITableViewCell is selected, it just appears after the cell receives a long press (~ 15 seconds) and crashes. If I open again it works well, or without the long press, if I just build it again it works as expected too.
Here are the methods:
-(void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSLog(#"Cell is selected");
self.popUpViewController = [[PopupViewController alloc] initWithNibName:#"PopupViewController" bundle:nil];
self.popUpViewController.message = [self.arrayOfMessages objectAtIndex:indexPath.row];
[self presentTransparentModalViewController:self.popUpViewController animated:YES withAlpha:1];
}
You can check the called method below:
-(void) presentTransparentModalViewController: (PopupViewController *) aViewController
animated: (BOOL) isAnimated
withAlpha: (CGFloat) anAlpha{
self.popUpViewController = aViewController;
UIView *rootView = UIApplication.sharedApplication.delegate.window.rootViewController.view;
UIView *view = aViewController.view;
view.opaque = NO;
view.alpha = anAlpha;
[view.subviews enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
UIView *each = obj;
each.opaque = NO;
each.alpha = anAlpha;
}];
if (isAnimated) {
//Animated
CGRect mainrect = rootView.bounds;
/* In case you want to add to self.view, use the following instead of the line
above:
CGRect mainrect = [[UIScreen mainScreen] bounds];
*/
CGRect newRect = CGRectMake(0, mainrect.size.height, mainrect.size.width, mainrect.size.height);
[rootView addSubview:view];
/* In case you want to add to self.view, use the following instead of the line
above:
[self.view addSubview:view];
*/
view.frame = newRect;
[UIView animateWithDuration:0.25
animations:^{
view.frame = mainrect;
} completion:^(BOOL finished) {
//
}];
}else{
view.frame = [[UIScreen mainScreen] bounds];
[rootView addSubview:view];
}
}
DETAIL: If the view is added as subview of self.view it works as expected. Why it doesn't work for rootViewController.view?
Does anyone knows why it happens and / or how to fix it?
EDIT:
I checked to see if the view is added to rootView.
So, I have rootViewController.view add a subview with transparent background to it.
What should happens: We should see the subview, right?
What actually happens: We don't see any visual change.
How do I know the view is there: If I create an empty view and add my rootView to it, it's shown there.
It happens only after installation, in the first build.
Best regards,
Arthur A.

In iOS7.1 UIButton set image not working?

[_firstPrincipleButton setBackgroundImage:[UIImage imageNamed:#"i_press.png"]
forState:UIControlStateNormal];
The above code is working fine in iOS7 but in iOS7.1 this is not changing the image of UIButton. I have also tried to do this with setImage but didn't work.
Thanks
Just in case that others have the same problem and need another hint on how to fix this:
I also had this problem and finally found out that setting a button's (background) image doesn't work on iOS 7.1, if the button is disabled.
Not sure if this fixes your problem, but it was mine. Calling setNeedsLayout didn't help in my case, unfortunately. What you can do as a workaround is either to override UIButton or to add a category that contains a method like the following to set an image:
- (void)setImage:(UIImage *)img forButtonState:(UIControlState)state
{
BOOL shouldEnable = self.enabled;
self.enabled = YES;
[self setImage:img forState:state];
self.enabled = shouldEnable;
}
Filed a bug report for this issue (16497031).
It is a bug in iOS 7.1 ,
you can create a category for UIButton and implement a method to set the image somewhat like this.
-(void)setImage:(UIImage *)image forButtonState:(UIControlState)state
{
CGFloat version = [[UIDevice currentDevice] systemVersion].floatValue ;
if( version <= 7.1)
{
BOOL isFirstTime = YES ;
for(UIView *aView in self.subviews)
{
if([aView isKindOfClass:[UIImageView class]])
{
[(UIImageView *)aView setImage:image] ;
isFirstTime = NO ;
break ;
}
}
if(isFirstTime)
{
CGRect frame = self.frame ;
frame.size = image.size ;
UIImageView * imageView = [[UIImageView alloc] initWithFrame:frame];
[imageView setImage:image];
[self addSubview:imageView];
}
}
else
[self setImage:image forState:state];
}
This will work.
enjoy coding :)

How to determine position of UIBarButtonItem in UIToolbar?

What's the easiest way to determine the x,y location of a UIBarButtonItem in a UIToolbar?
The only answer I found is in any way to know where uibarbuttonitem has been drawn.
All proposed answers seem too complicated. There ought to be a simpler way to get the position of the damn UIBarButtonItem isn't there?
I used this which seems to be the most elegant way
- (CGRect)frameForBarButtonItem:(UIBarButtonItem *)buttonItem
{
UIView *view = [buttonItem valueForKey:#"view"];
return view ? view.frame : CGRectZero;
}
Unfortunately, there is no easy way of determining the position of a UIBarButtonItem. A UIBarButtonItem is essentially a NSObject that does just two things: describe the look and feel of a toolbar button, and forward events to the designated target/action selector.
Now, given that all buttons are subviews of UIToolbar, and all button events are routed through their respective UIBarButtonItems, it's quite trivial to loop through all subviews of your UIToolbar and when you find a button whose target is that of your UIBarButtonItem, just get the frame of that button. Some code:
UIToolbar *toolbar = <your toolbar>;
UIBarButtonItem *barButtonItem = <your item>;
UIButton *button = nil;
for (UIView *subview in toolbar.subviews) {
if ([subview isKindOfClass:[UIButton class]]) {
for (id target in [(UIButton *)subview allTargets]) {
if (target == barButtonItem) {
button = (UIButton *)subview;
break;
}
}
if (button != nil) break;
}
}
CGRect frame = button.frame;
This works for me when using popovers from UIBarButtonItems.
- (void)buttonClicked:(id)sender{
{
CGRect buttonRect = [[sender view] frame];
}
For iOS 11 (and maybe above) the call:
if let view = barButtonItemView.value(forKey: "view") as? UIView {
let viewFrame = view.frame
}
will return a zero point origin for the view. To counter this, ask for the window coordinates of the view, by using:
let actualPointOnWindow = view.convert(view.frame.origin, to: nil)
If your toolbar has any translations subtract them from the calculated point to find its position on the toolbar.
Complete Code:
if let view = barButtonItemView.value(forKey: "view") as? UIView {
let viewFrame = view.frame
let actualPointOnWindow = view.convert(view.frame.origin, to: nil)
}
A more 'swifty' SWIFT way:
func userAction(sender: AnyObject) {
if let originView = sender.valueForKey("view") {
let frame = originView.frame //it's a UIBarButtonItem
} else {
let frame = sender.frame //it's a regular UIButton
}
}
UIBarButtonItem is NOT a subclass of UIView even though it's essentially a button. Go figure.
I used johosher's approach to get the position of a UIBarButtonItem using Swift.
Since I needed the position as sourceRect for a popoverPresentationController I had to convert it to self.view. I am not sure, whether this is a good solution but it works very well and the popover shows off right of the UIBarButtonItem.
let senderRect = sender.view!!.convertRect(sender.view!!.bounds, toView: self.view)
// additional code for the popover controller
alertController.modalPresentationStyle = UIModalPresentationStyle.Popover
alertController.popoverPresentationController?.sourceRect = senderRect
alertController.popoverPresentationController?.sourceView = self.view
UIToolbar *toolbar;
for (UIView *v in [toolbar items])
{
if ([v isKindOfClass:[UIBarButtonItem class]])
{
UIBarButtonItem *b = (UIBarButtonItem*)v;
//do something with b..
}
}

iPad, Resize custom button in viewForHeaderInSection

My application has splitview and it's activated only in landscape mode.
In masterview i have tableview and in the last section i have a custom button.
The problem is that the button width stays in potrait instead of using device orientation.
I've tried to play with setAutoresizingMask on button and view but that doesn't work either.
- (UIView *) tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger) sectionNum {
if (sectionNum == 4) {
CGRect frameSize;
if (UIInterfaceOrientationIsLandscape([[UIDevice currentDevice] orientation])) {
frameSize = CGRectMake(45.0, 0.0, 600.0, 44.0);
} else {
frameSize = CGRectMake(45.0, 0.0, 680.0, 44.0);
}
UIView *btnView = [[[UIView alloc] initWithFrame:frameSize] autorelease];
// button
UIButton* btnFind = [UIButton buttonWithType:UIButtonTypeCustom];
[btnFind setBackgroundImage:[[UIImage imageNamed:#"buttonblue.png"] stretchableImageWithLeftCapWidth:14.0 topCapHeight:0.0] forState:UIControlStateNormal];
[btnFind setBackgroundImage:[[UIImage imageNamed:#"buttonred.png"] stretchableImageWithLeftCapWidth:14.0 topCapHeight:0.0] forState:UIControlStateHighlighted];
btnFind.frame = frameSize;
btnFind.tag = 1;
[btnFind setTitle:#"Find" forState:UIControlStateNormal];
[btnFind addTarget:self action:#selector(doSearchDoc:) forControlEvents:UIControlEventTouchUpInside];
//[btnFind setAutoresizingMask:UIViewAutoresizingFlexibleRightMargin];
//[btnView setAutoresizingMask:UIViewAutoresizingFlexibleWidth];
[btnView addSubview:btnFind];
return btnView;
}
return nil;
}
Can someone please let me know how to resolve this.
Thanks.
I've had other issues that the header was not refreshed when rotating the device, ie. the method above was not invoked in my situatio, you could check if this is the case also for you.
The solution I adopted was to invoke reloadData in the rotating method :
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
// This solves an issue that the header doesn't show when roating device (ok on simulator)
NSIndexPath* ipselected = [self.tableView indexPathForSelectedRow];
[self.tableView reloadData];
[self.tableView selectRowAtIndexPath:ipselected animated:false scrollPosition:UITableViewScrollPositionMiddle];
}
Of course it is usable if you can afford a full reload when rotating the device but itis typically a time when the user will better accept a 0.5s delay as he/she is busy with moving the device.
lternatively you could keep an ivar to your button and change its frame in the above method.

Resources