Fix: UIButton Image Stretches to Full Size in Swift

Fix: UIButton Image Stretches to Full Size in Swift

Written by

in

Quick answer: In modern UIKit (iOS 15+), use UIButton.Configuration to control icon sizing. For SF Symbols, assign a UIImage.SymbolConfiguration to button.configuration?.preferredSymbolConfigurationForImage. For bitmap assets, adjust configuration.contentInsets or downscale the UIImage using byPreparingThumbnail(ofSize:) instead of relying on deprecated imageEdgeInsets.

Why this happens

When you assign an image to a UIButton using setImage(_:for:), UIKit calculates the button’s intrinsic content size based on the dimensions of the underlying image and any assigned title. If you apply layout constraints that force the button into a fixed frame (such as 44×44 or 100×100 points), the button’s internal UIImageView attempts to adapt to that frame according to its default layout rules.

By default, a standard UIButton does not scale bitmap assets proportionally down into a smaller sub-region of its bounding box. If the source asset has larger pixel dimensions than intended, or if the button frame forces the internal image view to expand, the image fills the entire button surface, often bleeding directly against the view edges or distorting its aspect ratio.

In older versions of UIKit, developers relied on imageEdgeInsets to push the image inward. Starting in iOS 15, Apple deprecated imageEdgeInsets and contentEdgeInsets in favor of UIButton.Configuration. Understanding how both the modern configuration system and legacy image views handle content scaling is key to keeping button icons at their intended dimensions.

How to fix it

1. Use UIButton.Configuration with SymbolConfiguration (iOS 15+)

If you are using SF Symbols, the most reliable and forward-compatible approach is to assign a symbol configuration directly to your button’s configuration object. This adjusts the point size, weight, and scale of the symbol without altering the button’s outer frame.

import UIKit

class HeartButtonViewController: UIViewController {
    let heartButton = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()
        setupButton()
    }

    private func setupButton() {
        // Create a configuration
        var config = UIButton.Configuration.plain()

        // Assign the SF Symbol image
        config.image = UIImage(systemName: "heart.fill")

        // Define exact symbol point size and scale
        let symbolConfig = UIImage.SymbolConfiguration(pointSize: 18, weight: .medium, scale: .medium)
        config.preferredSymbolConfigurationForImage = symbolConfig

        // Optional padding around the content
        config.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)

        heartButton.configuration = config
        heartButton.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(heartButton)

        NSLayoutConstraint.activate([
            heartButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            heartButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            heartButton.widthAnchor.constraint(equalToConstant: 44),
            heartButton.heightAnchor.constraint(equalToConstant: 44)
        ])
    }
}

Setting preferredSymbolConfigurationForImage ensures the symbol maintains crisp vector rendering at the specified point size, leaving the rest of the 44×44 frame as a comfortable tap target.

2. Resize Bitmap Images with byPreparingThumbnail(ofSize:)

UIImage.SymbolConfiguration only applies to vector symbol assets. If you are loading standard raster images (.png or .jpg) from an asset catalog or local file, setting symbol configurations will have no effect.

Instead of stretching a full-resolution bitmap inside the button, downscale the asset to the exact visual size you want using byPreparingThumbnail(ofSize:) or UIGraphicsImageRenderer:

import UIKit

func configureBitmapButton(button: UIButton, assetName: String, targetSize: CGSize) {
    guard let originalImage = UIImage(named: assetName) else {
        return
    }

    // Asynchronously generate a thumbnail or resize synchronously
    let renderer = UIGraphicsImageRenderer(size: targetSize)
    let resizedImage = renderer.image { _ in
        originalImage.draw(in: CGRect(origin: .zero, size: targetSize))
    }

    var config = UIButton.Configuration.plain()
    config.image = resizedImage
    config.contentInsets = NSDirectionalEdgeInsets(top: 8, leading: 8, bottom: 8, trailing: 8)
    button.configuration = config
}

For remote or asynchronous images, use the built-in thumbnail preparation API:

if let rawImage = UIImage(named: "custom-heart") {
    rawImage.byPreparingThumbnail(ofSize: CGSize(width: 24, height: 24)) { thumbnail in
        DispatchQueue.main.async {
            var config = self.myButton.configuration ?? UIButton.Configuration.plain()
            config.image = thumbnail
            self.myButton.configuration = config
        }
    }
}

3. Adjust ImageView ContentMode and Content Insets

When you need to keep a button’s frame fixed while preventing its image from distorting, adjust the button’s internal imageView content mode to .scaleAspectFit and add directional insets.

In modern configurations, insets are applied via NSDirectionalEdgeInsets on the UIButton.Configuration:

var config = UIButton.Configuration.borderless()
config.image = UIImage(named: "icon-heart")

// Add padding to prevent the image from filling the frame
config.contentInsets = NSDirectionalEdgeInsets(top: 12, leading: 12, bottom: 12, trailing: 12)
myButton.configuration = config

// Ensure the underlying imageView respects aspect ratio
myButton.imageView?.contentMode = .scaleAspectFit

By adding insets of 12 points to all four sides, a 44×44 point button restricts its content area to a 20×20 box, automatically sizing down the rendered asset without changing the touch boundary.

4. Legacy Solution: imageEdgeInsets (iOS 14 and earlier)

If your code base maintains support for iOS 14 or earlier without UIButton.Configuration, you can control the image dimensions using imageEdgeInsets and the button’s underlying image view:

let legacyButton = UIButton(type: .custom)
legacyButton.setImage(UIImage(named: "icon-heart"), for: .normal)

// Prevent image distortion
legacyButton.imageView?.contentMode = .scaleAspectFit

// Inset the image by 10 points on all sides
legacyButton.imageEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)

Note that imageEdgeInsets is formally deprecated starting in iOS 15. If a button has a non-nil configuration, setting imageEdgeInsets has no effect. Only use this pattern if your button does not adopt UIButton.Configuration.

How to prevent it

  • Provide multi-resolution image assets (@1x, @2x, @3x) scaled to the exact intended point dimensions in your asset catalog rather than dropping full-bleed artwork into button slots.
  • Prefer SF Symbols for UI icons, which adapt automatically through UIImage.SymbolConfiguration.
  • Adopt UIButton.Configuration across all iOS 15+ targets rather than mixing legacy imageEdgeInsets with new button layouts.
  • Maintain an outer button frame of at least 44×44 points for touch target accessibility while using internal insets to size smaller icons.
Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.