Search code examples
iosswift

How to change button image size with button configuration?


I am using this code to create a button with an image. And I want the image size to be the same as the button size. I am using this code for that:

func buttonImage() {
   var configuration = UIButton.Configuration.plain()
   configuration.image = UIImage(named: "someImage")!
   configuration.baseBackgroundColor = .red
   configuration.contentInsets = .zero
        
    let button = UIButton(configuration: configuration)
    button.backgroundColor = .red
    button.translatesAutoresizingMaskIntoConstraints = false
        
    view.addSubview(button)
        
    button.widthAnchor.constraint(equalToConstant: 300).isActive = true
    button.heightAnchor.constraint(equalToConstant: 300).isActive = true
    button.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
    button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
}

but I have this result:

enter image description here

All works fine if I use single scale with 135x135 without @3x suffix instead of individual scales with 3 images 45x45, 90x90, 135x135 in my assets.

How to fix it with using configuration?


Solution

  • A UIButton using UIButton.Configuration does not scale the image... it uses the image at its point size.

    If we have:

    and we run this code:

    guard let imgA = UIImage(named: "someImage"),
          let imgB = UIImage(named: "someImageU")
    else { fatalError() }
    
    print("imgA size:", imgA.size)
    print("imgB size:", imgB.size)
    

    we will get this output:

    imgA size: (45.0, 45.0)
    imgB size: (135.0, 135.0)
    

    Whether the device has a screen scale of 1, 2 or 3, the point size will always be the same, and UIKit will load the corresponding @1, @2 or @3 image.

    If you are using a png (or other format) image in your Asset Catalog, you can either save multiple sizes and do something like this:

    var btnImage: UIImage!
    if true {
        if UIScreen.main.scale == 1 {
            btnImage = UIImage(named: "someImage45x45")
        } else if UIScreen.main.scale == 2 {
            btnImage = UIImage(named: "someImage90x90")
        } else {
            btnImage = UIImage(named: "someImage135x135")
        }
    }
    

    Or, scale the UIImage on-the-fly before assigning it as the configuration's .image property...

    Or, create a UIButton without using UIButton.Configuration


    Edit

    Some clarification...

    As I said, when using UIButton.Configuration, the image point size will be used.

    If you want the button frame to be 300 x 300 points, and you want the image to be 135 x 135 points, the images in your should be:

    • @1x == 135 x 135
    • @2x == 270 x 270
    • @3x == 405 x 405

    (Actually, no devices have @1x screen scale, so you only need the @2x and @3x images.)

    That's fine, as long as you always want the button's image to be 135 x 135 points.

    Suppose, though, that you decide you want you button to be 200 x 200 points (2/3rds of 300), and thus your button's image should be 90 x 90 points (2/3rds of 135)?

    What you can do is resize the image on-the-fly.

    Keep in mind, though... if you have PNG images in your Asset Catalog, you will get bit-scaled images which may get "fuzzy" or otherwise lose detail.

    That's where using SVG files can be very helpful.

    Quick example -- using Sketch for Mac, I exported this as a 45x45 PNG:

    starPNG45x45

    and here is the SVG code:

    <svg width="45px" height="45px" viewBox="0 0 45 45" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
        <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
            <g id="Artboard">
                <rect id="Rectangle" fill="#32C5FF" x="0" y="0" width="45" height="45"></rect>
                <path d="M22.5,5.43914852 L26.5842329,18.8785356 L40.627947,18.609873 L29.1084277,26.6472083 L33.7036874,39.9205528 L22.5,31.4485121 L11.2963126,39.9205528 L15.8915723,26.6472083 L4.37205299,18.609873 L18.4157671,18.8785356 L22.5,5.43914852 Z" id="Star" stroke="#E02020" stroke-width="2" fill="#F7B500"></path>
            </g>
        </g>
    </svg>
    

    When adding the SVG to assets, select Preserve Vector Data...

    Then, using this resizing code:

    guard let imgA = UIImage(named: "starPNG45x45"),
          let imgB = UIImage(named: "starSVG45x45")
    else { fatalError() }
    
    print("imgA size:", imgA.size)
    print("imgB size:", imgB.size)
    
    let targetPointSize: CGFloat = 135.0
    
    let targetImageSize: CGSize = .init(width: targetPointSize, height: targetPointSize)
    
    // by default, UIGraphicsImageRenderer uses the screen-scale
    var rndr = UIGraphicsImageRenderer(size: targetImageSize)
    let rImgA = rndr.image { ctx in
        imgA.draw(in: .init(origin: .zero, size: targetImageSize))
    }
    let rImgB = rndr.image { ctx in
        imgB.draw(in: .init(origin: .zero, size: targetImageSize))
    }
    

    The resulting images (on an iPhone 15 Pro with 3x screen scale)...

    PNG:

    png result

    SVG:

    svg result

    Note that the generated images will each have a point size of 135, but a pixel size of 405.

    If we run that code on a device with 2x screen scale, the resulting images will still have a point size of 135, but a pixel size of 270.