52

I'm allocating a UIButtonTypeCustom to a UIView with a background image that is smaller than the button's frame.
Reason why the image is smaller is because I'm trying to add more of a "target area" for the UIButton.
However, the image is being scaled to the full size of the frame, rather than just being the image's size.

I have tried setting the UIButton and UIButton's imageView's contentMode property to UIViewContentModeScaleAspectFit, but no luck, the image still gets stretched out.

Is there a way to do what I'm trying to do programmatically?

Thanks in advance!

stackich
  • 3,607
  • 3
  • 17
  • 41
Raphael Caixeta
  • 7,808
  • 9
  • 51
  • 76
  • 4
    Do you really need it to be the background image? Do you need to actually write text or place another image on top of it ? If not, use the image and not background image, as only the background image is stretched by default to match the frame. – Ignacio Inglese Jan 11 '12 at 18:43
  • @Raphael Are none of these answers adequate? – memmons Oct 01 '13 at 20:00

12 Answers12

126

A lot of people make the same mistake you do in regards to button images and then jump through hoops trying to make the button behave as they expect it to. Let's clear this up once and for all:

A UIButton has two types of images it can display -- a foreground image and a background image. The background image for a button is expected to replace the button's background texture. As such, it makes sense that it stretches to fill the entire background. However, the button's foreground image is expected to be an icon that may or may not display alongside text; it will not stretch. It may shrink if the frame is smaller than the image, but it will not stretch. You can even set the alignment of the foreground image using the Control alignment properties in Interface Builder.

A button's foreground and background image can be set in code like this:

// stretchy
[self setBackgroundImage:backgroundImage forState:UIControlStateNormal];  

// not stretchy
[self setImage:forgroundImage forState:UIControlStateNormal]; 
memmons
  • 40,222
  • 21
  • 149
  • 183
  • 53
    What do you do when you need a non stretching image to be displayed with text on the top of it? – Zaky German Dec 23 '12 at 13:19
  • Weird, whenever I set the UIButton image property the title text disappears. Is there a way to prevent that? edit: Oh, the text is off to the right.. weird, I guess I'll just play with the adjustments until it is directly under the foreground image – powerj1984 May 28 '13 at 16:59
  • @powerj1984 If you want text sitting on top of an image, you should be using a backgroundImage. The foreground image is expected to replace text or sit alongside text. If you are adjusting your text to force it to sit on top of the foreground image you are doing it wrong. – memmons May 31 '13 at 02:42
  • @ZakyGerman If you need a non-stretching image sitting in the background you should still use the background image property. As long as you make your button the same size as the background image it won't stretch. – memmons May 31 '13 at 02:46
  • I definitely didn't want the text on top of the image. Turns out the text was just being moved to the right of the image and my UIButton wasn't showing it until I stretched it to the right. Then I just adjusted the content insets until the text was aligned below the image as I wanted. Thanks! – powerj1984 May 31 '13 at 13:24
  • 3
    I'm currently witnessing an image in my app set with 'setImage()' being stretched to fill the button. I don't know if the content of this answer is no longer true, or if there's something I don't know. I tend to assume the latter, but I thought it was worth mentioning. – Euroclydon37 Jun 20 '17 at 03:12
10

You don't have access to the background imageView, but there is fully working workaround:

EDIT: There is an even better workaround then what I posted originally. You can create a UIImage from any color, and call -setBackgroundImage:forState.

See bradley's answer, here: https://stackoverflow.com/a/20303841/1147286


Original answer:

Instead of calling -setBackgroundImage:forState:, create a new UIImageView and add it as a subview of the button.

UIImageView *bgImageView = [[UIImageView alloc] initWithImage:img];
bgImageView.contentMode = UIViewContentModeScaleAspectFit;
[bgImageView setFrame:CGRectMake(0, 0, videoButton.frame.size.width, videoButton.frame.size.height)];
bgImageView.tag = 99;
[yourButton addSubview:bgImageView];
[yourButton bringSubviewToFront:yourButton.imageView];
  1. Create the imageview
  2. Set the content mode and frame
  3. I also set a recognizable tag, so that when the screen rotates I can easily find my custom imageView in the button's subviews and reset its frame
  4. Add it as a subview to the button
  5. Bring the frontal imageView of the button to the front so our custom imageView doesn't overlap it

When the button needs to rotate just find the imageView by its tag and reset its frame:

UIImageView *bgImageView = (UIImageView *)[button viewWithTag:99];
[bgImageView setFrame:CGRectMake(0, 0, newWidth, newHeight)];
Community
  • 1
  • 1
Zoltán Matók
  • 3,923
  • 2
  • 33
  • 64
1

The cleanest and easiest way it probably to use the title insets of the button.

You set your image as the button image, and then you change the left title inset to match minus the width of your image:

myButton.titleEdgeInsets = UIEdgeInsetsMake(0, -myImage.width, 0, 0)

This will move the text back where it was before the image was added to its left. You can also use this value to add some padding to you button.

AntonyG
  • 224
  • 2
  • 4
  • @AntongG, man he is asking for image and you added the code for titleedgeinsets, :D also imageedgeinset won't work on background FYI. – Steve Mar 21 '22 at 06:39
1

Stumbled on this problem too.

Adding image programmatically, as memmons thoroughly explained, did not help:(

I had a button 100x40 and image 100x100, it would appear squeezed, not fitted, as one would infer from "Aspect Fit" option. Actually, non of those view options had an effect.

I just had to rescale it so it would fit on a button, then use setImage:

UIImage *img=[UIImage imageNamed:@"myimage.png"];
CGImageRef imgRef = [img CGImage];
CGFloat imgW = CGImageGetWidth(imgRef);
CGFloat imgH = CGImageGetHeight(imgRef);
CGFloat btnW = myBttn.frame.size.width;
CGFloat btnH = myBttn.frame.size.height;
//get lesser button dimension
CGFloat minBtn=btnW;
if (btnW>btnH) {
    minBtn=btnH;
}
//calculate scale using greater image dimension
CGFloat scl=imgH/minBtn;
if (imgW>imgH) {
    scl=imgW/minBtn;
}
//scale image
UIImage *scaledImage = [UIImage imageWithCGImage:[img CGImage] scale:(img.scale * scl) orientation:(img.imageOrientation)];
//clean up
UIGraphicsEndImageContext();
//set it on a button
[myBttn setImage:scaledImage forState:UIControlStateNormal];
Boris Gafurov
  • 1,427
  • 16
  • 28
1

It is simple as:

ImageBtn.imageView?.contentMode = .scaleAspectFit
ImageBtn.setImage(chosenImage, for: .normal)
stackich
  • 3,607
  • 3
  • 17
  • 41
Ronen
  • 1,225
  • 13
  • 10
  • This worked for me just be sure your button type is set to custom from the Attribute Inspector. – GordonW Jul 28 '20 at 21:10
0

Another consideration is the BaseLine constraint. If your buttons have this constraint set (depicted as a horizontal or vertical line through multiple controls on your layout), it will cause your images to stretch without stretching the underlying button control. If your button is otherwise properly constrained (leading/trailing and top/bottom spaces, and etc...) removing the BaseLine constraint should have no impact on the layout, while allowing the foreground image to scale properly to the underlying button shape.

0

Answerbot answers the question with what is proper and correct to do. Don't fight the OS and use things as intended is always good advice. However, sometimes you need to break the rules.

I was able to mask the enlarged background image (not prevent it) by overlaying it with a black CAlayer then overlaying again with a properly resized image CAlayer. This was all done by creating a subclass of UIButton and overwriting the setHighlighted method.

NEED CODE?

- (void)setHighlighted:(BOOL)highlighted
{
super.highlighted = highlighted;

//
//Whenever an image needs to be highlighted, create a dimmed new image that is correctly       sized. Below it is a englarged stretched image.
//
if (highlighted != _previousHighlightedSate)
{
    _previousHighlightedSate = highlighted;

    if (highlighted)
    {
        //Create a black layer so image can dim
        _blackLayer = [CALayer layer];
        _blackLayer.bounds = self.bounds;
        CGRect rect = _blackLayer.bounds;
        rect.size.width = rect.size.width*2;
        rect.size.height = rect.size.height*2;
        _blackLayer.bounds = rect;
        _blackLayer.backgroundColor = [[UIColor blackColor] CGColor];

        //create image layer
        _nonStretchImageLayer = [CALayer layer];
        _nonStretchImageLayer.backgroundColor = [UIColor blackColor].CGColor;
        _nonStretchImageLayer.bounds = CGRectMake(0  , 0, self.bounds.size.width, self.bounds.size.height);
        _nonStretchImageLayer.frame = CGRectMake(0  , 0, self.bounds.size.width, self.bounds.size.height);
        _nonStretchImageLayer.contentsGravity = kCAGravityResizeAspect;//default is to resize
        _nonStretchImageLayer.contents = (id)self.imageView.image.CGImage;
        _nonStretchImageLayer.opacity = 0.5;

        //add layers to image view
        [self.imageView.layer addSublayer:_blackLayer];
        [self.imageView.layer addSublayer:_nonStretchImageLayer];
    }
    else
    {
        //remove from image view
        [_blackLayer removeFromSuperlayer];
        [_nonStretchImageLayer removeFromSuperlayer];

        //nil them out.
        _blackLayer = nil;
        _nonStretchImageLayer = nil;
    }
}  

Inspiration for this work around came from here

LEO
  • 2,572
  • 1
  • 26
  • 31
0

I guess the easiest solution is to use UIImage's resizableImageWithCapInsets method. Use UIEdgeInsetsMake to configure the free spaces.

MacMark
  • 6,239
  • 2
  • 36
  • 41
0

might help someone

button.subviews.first?.contentMode = .scaleAspectFit
0

Swift version of Zoltán Matók answer

Just a copy of my code using SnapKit to do auto layout and syntatic sugar Then library for initilizations, it should work similar for normal Apples way of programatic layout.

   let backButton = UIButton(type: .custom).then { (button) in
        let image = UIImage(named: "backButtonIcon")?.withRenderingMode(.alwaysOriginal)
        let imageView = UIImageView(image: image)
        button.addSubview(imageView)
        imageView.snp.makeConstraints { (make) in
            make.center.equalTo(button.snp.center)
        }
        button.bringSubviewToFront(imageView)
        button.contentMode = .scaleAspectFit
        button.backgroundColor = .clear
        button.isUserInteractionEnabled = true
        button.isEnabled = true
        
        
        button.imageView?.contentMode = .scaleAspectFit
        button.imageView?.snp.makeConstraints({ (make) in
            make.height.width.equalTo(24)
        })
        button.snp.makeConstraints { (make) in
            make.height.width.equalTo(24)
        }
    }

Mussa Charles
  • 4,014
  • 2
  • 29
  • 24
0

You can use uibutton.imageView.contentMode for no stretching:

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.frame = CGRectMake(posX, posY, widthButton, heightButton);
    [button setTitle:@"" forState:UIControlStateNormal];
    [button setImage:[UIImage imageNamed:@"imageNamed"] forState:UIControlStateNormal];
    [button setImage:[UIImage imageNamed:@"imageNamedHighlighted"] forState:UIControlStateHighlighted];
    button.imageView.contentMode = UIViewContentModeScaleAspectFit;
    [button addTarget:self action:@selector(functionMenu:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview: button];
Alessandro Pirovano
  • 2,509
  • 28
  • 21
-1

What you need to do is add your image as a UIImageView.

Than add a button with transperent background (UIColor ClearColor) on top of it with your desired width and height.

ozba
  • 6,522
  • 4
  • 33
  • 40
  • 1
    That is actually a bad suggestion, as the image will not be part of the button and will not reflect state changes (which would defeat the purpose of having an image as part of the button). – GtotheB Mar 12 '13 at 00:15
  • For that you can change the image when the button is tapped in order to mimic the state change – ozba Mar 12 '13 at 08:35
  • 1
    @ozba State changes for images are already supported on the button. Adding a background image behind a clear button to mimic a button with an image is almost never a good idea and usually a really bad one. – memmons Sep 05 '13 at 14:38