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, orcontentEdgeInsetson buttons running on iOS 15 or later; these properties are deprecated and do nothing when a configuration is active. - Never read
button.titleLabel.boundsorbutton.imageView.boundsinsideviewDidLoad()to calculate insets, as views do not have resolved frames until layout passes run. - When building custom button designs with custom subviews, use a
UIStackViewinside the button or use a customUIControlsubclass 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.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
