Fix: UIButton Image and Title Spacing Issues in Swift

Fix: UIButton Image and Title Spacing Issues in Swift

Written by

in

Quick answer: In iOS 15 and later, set the button’s configuration to use configuration.imagePadding alongside configuration.imagePlacement. For legacy buttons without configuration, apply symmetrical half-width values to both imageEdgeInsets and titleEdgeInsets, and expand contentEdgeInsets by that same half-width to prevent text truncation.

Why this happens

Setting uibutton image and title spacing has historically caused layout bugs in UIKit because developers treat imageEdgeInsets and titleEdgeInsets like CSS margins or flexbox gap properties. In UIKit’s legacy layout system, edge insets do not insert space between subviews. Instead, positive inset values shrink the drawing rectangle of the target element inward toward the center, while negative inset values expand it outward.

When you adjust only titleEdgeInsets.left to push text away from an image, the button does not expand its overall width. Instead, UIKit shifts the label’s frame inside the button while preserving the old content boundary. This causes the title label to clip, truncate with trailing ellipses (...), or fail to respect sizeToFit() and Auto Layout intrinsic content sizing.

Furthermore, Apple deprecated imageEdgeInsets, titleEdgeInsets, and contentEdgeInsets in iOS 15. If a button uses modern styling such as UIButton.Configuration, UIKit ignores manual edge inset modifications entirely. Trying to mix legacy insets with modern configuration instances leads to ignored values or unpredictable shifts during state transitions.

How to fix it

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

The modern and recommended solution is to use UIButton.Configuration. The configuration API includes a dedicated imagePadding property that automatically calculates correct layout metrics, prevents truncation, and works seamlessly with Auto Layout.

import UIKit

func configureModernButton() -> UIButton {
    var config = UIButton.Configuration.filled()
    config.title = "Add to Favorites"
    config.image = UIImage(systemName: "star.fill")

    // Space between icon and text in points
    config.imagePadding = 8.0
    config.imagePlacement = .leading

    let button = UIButton(configuration: config)
    button.translatesAutoresizingMaskIntoConstraints = false
    return button
}

UIButton.Configuration automatically recalculates the button’s intrinsicContentSize. When the text length increases or Dynamic Type scales up, the 8-point gap remains consistent without clipping the title.

2. Position the Image Relative to the Title

If you need the image on the trailing edge (right of the text) or stacked vertically above the text, do not perform negative inset calculations. Instead, update config.imagePlacement:

import UIKit

func configureTrailingIconButton() -> UIButton {
    var config = UIButton.Configuration.tinted()
    config.title = "Continue"
    config.image = UIImage(systemName: "arrow.right")

    // Place image on the right with custom spacing
    config.imagePlacement = .trailing
    config.imagePadding = 10.0

    let button = UIButton(configuration: config)
    button.translatesAutoresizingMaskIntoConstraints = false
    return button
}

For a vertical layout where the icon sits on top of a multi-line title, change the placement to .top and adjust the text alignment:

import UIKit

func configureVerticalButton() -> UIButton {
    var config = UIButton.Configuration.plain()
    config.title = "Favorites"
    config.subtitle = "Quick Access"
    config.image = UIImage(systemName: "bookmark")

    config.imagePlacement = .top
    config.imagePadding = 6.0
    config.titleAlignment = .center

    let button = UIButton(configuration: config)
    button.translatesAutoresizingMaskIntoConstraints = false
    return button
}

3. Handle Multi-Line Titles with Configuration

If your button contains multiple lines of text beside an icon, legacy insets often fail because titleLabel.bounds reports zero width prior to layout. With UIButton.Configuration, you can configure line breaking on the title container directly using titleTextAttributesTransformer or by utilizing title and subtitle pairings:

import UIKit

func configureMultiLineButton() -> UIButton {
    var config = UIButton.Configuration.bordered()
    config.image = UIImage(systemName: "heart.fill")
    config.imagePlacement = .leading
    config.imagePadding = 12.0

    // Primary line and secondary line
    config.title = "Add To"
    config.subtitle = "Favorites"
    config.titleAlignment = .leading

    let button = UIButton(configuration: config)
    button.translatesAutoresizingMaskIntoConstraints = false
    return button
}

4. Symmetric Insets for Legacy Buttons (Pre-iOS 15)

If you maintain legacy views that cannot yet adopt UIButton.Configuration, you must adjust three properties simultaneously: imageEdgeInsets, titleEdgeInsets, and contentEdgeInsets. Applying symmetrical half-width values shifts the elements without shrinking their layout frames, and expanding the content insets provides the extra canvas width required.

Add this helper extension to your project:

import UIKit

extension UIButton {
    /// Adjusts spacing between image and title for legacy buttons without UIButton.Configuration.
    func setLegacyImageTitleSpacing(_ spacing: CGFloat) {
        let halfSpacing = spacing / 2.0

        // Shift image to the left
        imageEdgeInsets = UIEdgeInsets(top: 0, left: -halfSpacing, bottom: 0, right: halfSpacing)

        // Shift title to the right
        titleEdgeInsets = UIEdgeInsets(top: 0, left: halfSpacing, bottom: 0, right: -halfSpacing)

        // Expand the outer content bounds so neither the image nor title clips
        contentEdgeInsets = UIEdgeInsets(top: 0, left: halfSpacing, bottom: 0, right: halfSpacing)
    }
}

Use this method after assigning both the title and image:

let button = UIButton(type: .system)
button.setTitle("Add To Favorites", for: .normal)
button.setImage(UIImage(systemName: "star"), for: .normal)
button.setLegacyImageTitleSpacing(8.0)

How to prevent it

  • Migrate all UIKit buttons to UIButton.Configuration (.plain(), .filled(), .tinted(), or .bordered()) to avoid legacy frame math.
  • Avoid manually setting imageEdgeInsets, titleEdgeInsets, or contentEdgeInsets on buttons running on iOS 15 or later; these properties are deprecated and do nothing when a configuration is active.
  • Never read button.titleLabel.bounds or button.imageView.bounds inside viewDidLoad() to calculate insets, as views do not have resolved frames until layout passes run.
  • When building custom button designs with custom subviews, use a UIStackView inside the button or use a custom UIControl subclass with Auto Layout constraints instead of manipulating insets.

FAQ

Why does my title get truncated with ellipses when I adjust insets?

Legacy titleEdgeInsets shifts the title’s drawing position without increasing the button’s intrinsic content size. Because the button does not expand to accommodate the offset, UIKit truncates the label text to fit within the original, unexpanded frame.

Why does setting imageEdgeInsets have no effect in iOS 15 and later?

Apple introduced UIButton.Configuration in iOS 15 and deprecated legacy inset properties. If your button initializes with a modern configuration or system style, UIKit ignores legacy insets in favor of configuration.imagePadding.

How do I swap the positions of the image and title in a button?

With UIButton.Configuration, set config.imagePlacement = .trailing to put the icon on the right side of the text. For vertical stacking, set config.imagePlacement = .top or config.imagePlacement = .bottom.

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.