Ярлык под изображением в UIButton

Я пытаюсь создать кнопку, у которой есть текст под значком (вроде как кнопки приложения), но это кажется довольно сложным. Есть идеи, как я могу получить текст для отображения ниже изображения с UIButton?


person NRaf    schedule 17.11.2010    source источник
comment
Довольно просто и выполнимо создать собственный подкласс UIbutton, содержащий UIImage и UILabel, расположенный так, как вам нужно ...   -  person NP Compete    schedule 17.11.2010
comment
Или просто используйте UIButton и UILabel.   -  person raidfive    schedule 17.11.2010
comment
Чтобы точно контролировать размер и автоматический макет, вы можете попробовать следующее: https://github.com/albert-zhang/AZCenterLabelButton (Ссылка)   -  person Albert Zhang    schedule 18.12.2014
comment
отлично работает с этим решением stackoverflow.com/a/59666154/1576134   -  person Shreyank    schedule 09.01.2020


Ответы (33)


Или вы можете просто использовать эту категорию:

ObjC

@interface UIButton (VerticalLayout)

- (void)centerVerticallyWithPadding:(float)padding;
- (void)centerVertically;

@end

@implementation UIButton (VerticalLayout)

- (void)centerVerticallyWithPadding:(float)padding {
    CGSize imageSize = self.imageView.frame.size;
    CGSize titleSize = self.titleLabel.frame.size;
    CGFloat totalHeight = (imageSize.height + titleSize.height + padding);
    
    self.imageEdgeInsets = UIEdgeInsetsMake(- (totalHeight - imageSize.height),
                                            0.0f,
                                            0.0f,
                                            - titleSize.width);
    
    self.titleEdgeInsets = UIEdgeInsetsMake(0.0f,
                                            - imageSize.width,
                                            - (totalHeight - titleSize.height),
                                            0.0f);
    
    self.contentEdgeInsets = UIEdgeInsetsMake(0.0f,
                                              0.0f,
                                              titleSize.height,
                                              0.0f);
}

- (void)centerVertically {
    const CGFloat kDefaultPadding = 6.0f;
    [self centerVerticallyWithPadding:kDefaultPadding];
}

@end

Быстрое расширение

extension UIButton {
    
    func centerVertically(padding: CGFloat = 6.0) {
        guard
            let imageViewSize = self.imageView?.frame.size,
            let titleLabelSize = self.titleLabel?.frame.size else {
            return
        }
        
        let totalHeight = imageViewSize.height + titleLabelSize.height + padding
        
        self.imageEdgeInsets = UIEdgeInsets(
            top: -(totalHeight - imageViewSize.height),
            left: 0.0,
            bottom: 0.0,
            right: -titleLabelSize.width
        )
        
        self.titleEdgeInsets = UIEdgeInsets(
            top: 0.0,
            left: -imageViewSize.width,
            bottom: -(totalHeight - titleLabelSize.height),
            right: 0.0
        )
        
        self.contentEdgeInsets = UIEdgeInsets(
            top: 0.0,
            left: 0.0,
            bottom: titleLabelSize.height,
            right: 0.0
        )
    }
    
}

Предложение. Если высота кнопки меньше totalHeight, изображение будет выходить за пределы границ.

imageEdgeInset.top должно быть:

max(0, -(totalHeight - imageViewSize.height))
person Rafa de King    schedule 24.03.2014
comment
Я думаю, что это лучший ответ, поскольку он использует edgeInsets вместо ручной настройки кадра. Он также отлично работает с автоматическим макетом при вызове из layoutSubviews в супервизоре кнопки. Единственное предложение - использовать CGRectGetHeight() и CGRectGetWidth() при получении высоты и ширины imageView и titleLabel. - person Jesse; 24.10.2014
comment
Когда я использую это, изображение появляется над кнопкой, я должен CGFloat inset = (self.frame.size.height - totalHeight)/2; self.contentEdgeInsets = UIEdgeInsetsMake(inset, 0.0f, inset, 0.0f); расположить его по центру. - person Alex Hedley; 04.08.2017
comment
Расширение Swift показало мне некорректную компоновку. - person Patrick; 09.08.2018
comment
Он работает, если Image был установлен как setImage, а не как setBackgroundImage. - person Argus; 31.10.2018
comment
Решение не работает для меня. В imageViewSize и TitleLabelSize устанавливаются следующие значения: mageViewSize = (CGSize) (ширина = 0, высота = 0), titleLabelSize (CGSize) (ширина = 0, высота = 18) - person Spasitel; 13.10.2019
comment
Вы имеете в виду центр по горизонтали? OP описал, что они хотят, чтобы заголовок был под изображением .. что означало бы, что они центрированы по горизонтали, а не по вертикали. - person vikzilla; 07.11.2019
comment
отлично работает с этим решением stackoverflow.com/a/59666154/1576134 - person Shreyank; 09.01.2020
comment
Я смог заставить это сделать то, что мне было нужно, закомментировав часть, которая изменяла contentEdgeInsets. Вам нужно вызвать этот метод вместо didLayoutSubviews, иначе позиция будет неустойчивой (в моем случае вправо и немного вниз). - person Oscar; 03.03.2020

В Xcode вы можете просто установить для параметра Edge Title Left Inset отрицательное значение ширины изображения. Это отобразит метку в центре изображения.

Чтобы метка отображалась под изображением (по типу кнопок приложения), вам может потребоваться установить для параметра Edge Title Top Inset некоторое положительное число.

Изменить: вот код для достижения этого без использования Interface Builder:

/// This will move the TitleLabel text of a UIButton to below it's Image and Centered.
/// Note: No point in calling this function before autolayout lays things out.
/// - Parameter padding: Some extra padding to be applied
func centerVertically(padding: CGFloat = 18.0) {
    // No point in doing anything if we don't have an imageView size
    guard let imageFrame = imageView?.frame else { return }
    titleLabel?.numberOfLines = 0
    titleEdgeInsets.left = -(imageFrame.width + padding)
    titleEdgeInsets.top = (imageFrame.height + padding)
}

Обратите внимание, что это не сработает, если вы используете автоматическое размещение, а кнопка еще не была размещена на экране из-за ограничений.

person Chris    schedule 12.07.2012
comment
Это способ сделать это ... если вы не делаете это неоднократно с несколькими кнопками (разных размеров) ... в этом случае у меня были хорошие результаты с настроенной версией решения Эрика В. - person Kenny Winker; 12.07.2013
comment
Просто чтобы люди это понимали. Значение должно быть отрицательной шириной изображения, даже если кнопка шире, чем ширина изображения. - person Liron; 09.09.2013
comment
У меня это не сработало. Мой текст по-прежнему отображается справа от изображения, то есть не переносится под ним. - person Erika Electra; 30.10.2013
comment
@Cindeselia Это удивительно. Насколько большое значение вы использовали для верхней вставки? Может быть, попробовать увеличить его до еще большего значения? - person Chris; 01.11.2013
comment
к сожалению, есть различия в выравнивании между версиями ios с этим методом - person Radu Simionescu; 05.04.2014
comment
В iOS7 вроде не работает. Ярлык перемещается только в нижнюю часть изображения и скрывается, больше не отображается. - person Brave; 07.06.2014
comment
Я использовал метод вставки краев, предложенный Крисом, но поскольку ширина кнопки была меньше общей ширины изображения и заголовка кнопки, заголовок отображался как 3 точки (...). Я не мог увеличить ширину кнопки из-за разметки. Я решил это, изменив атрибут Line Break на: Clip. Это заставляет заголовок опускаться под изображением. Затем я исправил его расположение с помощью вставок. Это работает только при наличии указанной выше проблемы. - person Shaked Sayag; 27.05.2015
comment
Мне пришлось использовать отрицательную ширину рамки кнопки, а не отрицательную ширину рамки изображения кнопки. - person Chucky; 24.07.2017
comment
это не работает. со вставками размещение совершенно другое на устройстве / симуляторе и не соответствует таковому в IB - person Hogdotmac; 12.09.2017
comment
У меня проблема в том, что изображение оказывается выровненным по левому краю. Это плохо, когда кнопка оказывается шире изображения ... - person Nicolas Miari; 03.04.2018
comment
почему negative the width of the image сделает текст до середины? - person zionpi; 13.09.2020
comment
@zionpi, потянув этикетку по оси x в отрицательном направлении, переместит этикетку влево. - person C0D3; 28.09.2020
comment
Я думаю, что это должен быть выбранный ответ, поскольку другие ответы с функцией расширения UIButton centerVertical () у меня не работали. Я вкратце попробовал создать подклассы, но это, похоже, потребовало много работы для чего-то простого, чего я хотел достичь. Изменение заголовкаEdgeInsets.left и top вроде работает! - person C0D3; 28.09.2020

Это простая кнопка заголовка с центрированием, реализованная в Swift путем переопределения titleRect(forContentRect:) и imageRect(forContentRect:). Он также реализует intrinsicContentSize для использования с AutoLayout.

import UIKit

class CenteredButton: UIButton
{
    override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
        let rect = super.titleRect(forContentRect: contentRect)

        return CGRect(x: 0, y: contentRect.height - rect.height + 5,
            width: contentRect.width, height: rect.height)
    }

    override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
        let rect = super.imageRect(forContentRect: contentRect)
        let titleRect = self.titleRect(forContentRect: contentRect)

        return CGRect(x: contentRect.width/2.0 - rect.width/2.0,
            y: (contentRect.height - titleRect.height)/2.0 - rect.height/2.0,
            width: rect.width, height: rect.height)
    }

    override var intrinsicContentSize: CGSize {
        let size = super.intrinsicContentSize

        if let image = imageView?.image {
            var labelHeight: CGFloat = 0.0

            if let size = titleLabel?.sizeThatFits(CGSize(width: self.contentRect(forBounds: self.bounds).width, height: CGFloat.greatestFiniteMagnitude)) {
                labelHeight = size.height
            }

            return CGSize(width: size.width, height: image.size.height + labelHeight + 5)
        }

        return size
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        centerTitleLabel()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        centerTitleLabel()
    }

    private func centerTitleLabel() {
        self.titleLabel?.textAlignment = .center
    }
}
person simeon    schedule 03.10.2015
comment
Это самое правильное решение. Но некоторые модификации необходимы для внутреннего размера контента. Он должен возвращать МАКСИМАЛЬНУЮ ширину между изображением и меткой: return CGSizeMake (MAX (labelSize.width, self.imageView.image.size.width), self.imageView.image.size.height + labelHeight) - person kirander; 26.11.2015

Посмотрите на этот отличный ответ на Swift.

extension UIButton {

    func alignImageAndTitleVertically(padding: CGFloat = 6.0) {
        let imageSize = self.imageView!.frame.size
        let titleSize = self.titleLabel!.frame.size
        let totalHeight = imageSize.height + titleSize.height + padding

        self.imageEdgeInsets = UIEdgeInsets(
            top: -(totalHeight - imageSize.height),
            left: 0,
            bottom: 0,
            right: -titleSize.width
        )

        self.titleEdgeInsets = UIEdgeInsets(
            top: 0,
            left: -imageSize.width,
            bottom: -(totalHeight - titleSize.height),
            right: 0
        )
    }

}
person Tiago    schedule 08.02.2016
comment
Если вы также хотите, чтобы изображение было центрировано по вертикали, замените left в imageEdgeInsets на (self.frame.size.width - imageSize.width) / 2 - person elsurudo; 20.09.2016
comment
Если вы используете автоматическое размещение, вызовите этот метод в layoutSubviews() вашего супервизора. - person AlexVogel; 16.08.2019
comment
Кто устанавливает рамку imageView? Не лучше бы вы использовали imageView?.image.size? - person Zorayr; 17.08.2020

Подкласс UIButton. Переопределите -layoutSubviews, чтобы переместить встроенный subviews в новые позиции:

- (void)layoutSubviews
{
    [super layoutSubviews];

    CGRect frame = self.imageView.frame;
    frame = CGRectMake(truncf((self.bounds.size.width - frame.size.width) / 2), 0.0f, frame.size.width, frame.size.height);
    self.imageView.frame = frame;

    frame = self.titleLabel.frame;
    frame = CGRectMake(truncf((self.bounds.size.width - frame.size.width) / 2), self.bounds.size.height - frame.size.height, frame.size.width, frame.size.height);
    self.titleLabel.frame = frame;
}
person Dave Batton    schedule 20.02.2012
comment
Мне лично пришлось установить для titleLabel значение y равным 0, а высоту - равной высоте кадра, чтобы он отображал текст с изображением. Для меня это не имеет смысла, но это работает ... хотя я все еще изучаю способ настройки элементов управления «Apple». - person Russ; 07.03.2012
comment
На самом деле, лучший способ - переопределить titleRectForContentRect и imageRectForContentRect - person Mazyod; 05.08.2015

Рефакторинг ответа icecrystal23.

Swift 3, работает с авторазложениями, xib, раскадровками, можно анимировать.

Кнопка в исходном ответе icecrystal23 имела плохо рассчитанный фрейм. Думаю, я это исправил.

Изменить: обновлен до Swift 5 и работает внутри Interface Builder / Storyboards.

Изменить: обновлено для поддержки локализации (ltr / rtl) согласно комментарию Александра Акерса к ответу ниже.

import UIKit

@IBDesignable
class VerticalButton: UIButton {

    @IBInspectable public var padding: CGFloat = 20.0 {
        didSet {
            setNeedsLayout()
        }
    }
    
    override var intrinsicContentSize: CGSize {
        let maxSize = CGSize(width: CGFloat.greatestFiniteMagnitude, height: CGFloat.greatestFiniteMagnitude)
        
        if let titleSize = titleLabel?.sizeThatFits(maxSize), let imageSize = imageView?.sizeThatFits(maxSize) {
            let width = ceil(max(imageSize.width, titleSize.width))
            let height = ceil(imageSize.height + titleSize.height + padding)
            
            return CGSize(width: width, height: height)
        }
        
        return super.intrinsicContentSize
    }
    
    override func layoutSubviews() {
        if let image = imageView?.image, let title = titleLabel?.attributedText {
            let imageSize = image.size
            let titleSize = title.size()
            
            if effectiveUserInterfaceLayoutDirection == .leftToRight {
                titleEdgeInsets = UIEdgeInsets(top: 0.0, left: -imageSize.width, bottom: -(imageSize.height + padding), right: 0.0)
                imageEdgeInsets = UIEdgeInsets(top: -(titleSize.height + padding), left: 0.0, bottom: 0.0, right: -titleSize.width)
            }
            else {
                titleEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: -(imageSize.height + padding), right: -imageSize.width)
                imageEdgeInsets = UIEdgeInsets(top: -(titleSize.height + padding), left: -titleSize.width, bottom: 0.0, right: 0.0)
            }
        }
        
        super.layoutSubviews()
    }

}
person Kamil Harasimowicz    schedule 30.05.2017
comment
При удалении изображения возникает проблема. Я использую изображение для выбранного состояния и не использую изображение для состояния по умолчанию. Когда состояние изменяется с выбранного на значение по умолчанию, метка искажается. Поэтому необходимо несколько исправлений: не проверять просмотр изображения, а использовать «изображение (для: состояния)». Установите нулевые вставки по краю, если в операторе else макета Subviews нет изображения. - person Matic Oblak; 18.09.2017
comment
Единственное решение, которое работает. Другие ответы, похоже, работают, но на самом деле границы кнопок не меняются в соответствии с размером метки и изображения. Установите цвет фона, чтобы увидеть это. - person Manuel; 27.11.2018
comment
Это решение не сработало, я думаю, это вызвало какой-то бесконечный цикл и, в конечном итоге, сбой Xcode. Я удалил часть intrinsicContentSize, и она работала нормально (Xcode 11.5) - person mojuba; 09.07.2020
comment
И вы можете поддержать свой contentEdgeInsets, вычислив CGSize следующим образом: CGSize(width: width + contentEdgeInsets.left + contentEdgeInsets.right, height: height + contentEdgeInsets.top + contentEdgeInsets.bottom) - person androidguy; 20.10.2020

исправил один из ответов здесь:

Swift 3:

class CenteredButton: UIButton
{
    override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
        let rect = super.titleRect(forContentRect: contentRect)
        let imageRect = super.imageRect(forContentRect: contentRect)

        return CGRect(x: 0, y: imageRect.maxY + 10,
                      width: contentRect.width, height: rect.height)
    }

    override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
        let rect = super.imageRect(forContentRect: contentRect)
        let titleRect = self.titleRect(forContentRect: contentRect)

        return CGRect(x: contentRect.width/2.0 - rect.width/2.0,
                      y: (contentRect.height - titleRect.height)/2.0 - rect.height/2.0,
                      width: rect.width, height: rect.height)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        centerTitleLabel()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        centerTitleLabel()
    }

    private func centerTitleLabel() {
        self.titleLabel?.textAlignment = .center
    }
}
person Alex Shubin    schedule 10.03.2017

Решение Дэйва в Swift:

override func layoutSubviews() {
    super.layoutSubviews()
    if let imageView = self.imageView {
        imageView.frame.origin.x = (self.bounds.size.width - imageView.frame.size.width) / 2.0
        imageView.frame.origin.y = 0.0
    }
    if let titleLabel = self.titleLabel {
        titleLabel.frame.origin.x = (self.bounds.size.width - titleLabel.frame.size.width) / 2.0
        titleLabel.frame.origin.y = self.bounds.size.height - titleLabel.frame.size.height
    }
}
person Bartosz Hernas    schedule 24.09.2014
comment
Хороший ответ. Добавьте @IBDesignable в свой подкласс и просмотрите его в раскадровке. - person Joel Teply; 07.06.2016

Если вы создаете подклассы UIButton и override layoutSubviews, вы можете использовать приведенный ниже код для центрирования изображения и размещения заголовка по центру под ним:

kTextTopPadding - это константа, которую вам нужно будет ввести, которая определяет расстояние между изображением и текстом под ним.

-(void)layoutSubviews {
    [super layoutSubviews];

    // Move the image to the top and center it horizontally
    CGRect imageFrame = self.imageView.frame;
    imageFrame.origin.y = 0;
    imageFrame.origin.x = (self.frame.size.width / 2) - (imageFrame.size.width / 2);
    self.imageView.frame = imageFrame;

    // Adjust the label size to fit the text, and move it below the image
    CGRect titleLabelFrame = self.titleLabel.frame;
    CGSize labelSize = [self.titleLabel.text sizeWithFont:self.titleLabel.font
                                        constrainedToSize:CGSizeMake(self.frame.size.width, CGFLOAT_MAX)
                                        lineBreakMode:NSLineBreakByWordWrapping];
    titleLabelFrame.size.width = labelSize.width;
    titleLabelFrame.size.height = labelSize.height;
    titleLabelFrame.origin.x = (self.frame.size.width / 2) - (labelSize.width / 2);
    titleLabelFrame.origin.y = self.imageView.frame.origin.y + self.imageView.frame.size.height + kTextTopPadding;
    self.titleLabel.frame = titleLabelFrame;

}
person Erik W    schedule 03.04.2013

Это модифицированная версия отличного ответа Эрика В. Но вместо того, чтобы помещать изображение по центру в ВЕРХНЕЙ части представления, он помещает изображение и метку по центру представления как группу.

Разница в следующем:

+-----------+
|    ( )    |
|   Hello   |     // Erik W's code
|           |
|           |
+-----------+

vs

+-----------+
|           |
|    ( )    |     // My modified version
|   Hello   |
|           |
+-----------+

Источник ниже:

-(void)layoutSubviews {
    [super layoutSubviews];

    CGRect titleLabelFrame = self.titleLabel.frame;
    CGSize labelSize = [self.titleLabel.text sizeWithFont:self.titleLabel.font constrainedToSize:CGSizeMake(self.frame.size.width, CGFLOAT_MAX) lineBreakMode:NSLineBreakByWordWrapping];

    CGRect imageFrame = self.imageView.frame;

    CGSize fitBoxSize = (CGSize){.height = labelSize.height + kTextTopPadding +  imageFrame.size.height, .width = MAX(imageFrame.size.width, labelSize.width)};

    CGRect fitBoxRect = CGRectInset(self.bounds, (self.bounds.size.width - fitBoxSize.width)/2, (self.bounds.size.height - fitBoxSize.height)/2);

    imageFrame.origin.y = fitBoxRect.origin.y;
    imageFrame.origin.x = CGRectGetMidX(fitBoxRect) - (imageFrame.size.width/2);
    self.imageView.frame = imageFrame;

    // Adjust the label size to fit the text, and move it below the image

    titleLabelFrame.size.width = labelSize.width;
    titleLabelFrame.size.height = labelSize.height;
    titleLabelFrame.origin.x = (self.frame.size.width / 2) - (labelSize.width / 2);
    titleLabelFrame.origin.y = fitBoxRect.origin.y + imageFrame.size.height + kTextTopPadding;
    self.titleLabel.frame = titleLabelFrame;
}

К вашему сведению: это может сломаться при сочетании с анимацией UIView, поскольку во время них вызывается layoutSubviews.

person Kenny Winker    schedule 11.07.2013
comment
Разве строка, вычисляющая labelSize, не должна использовать self.bounds.size.width вместо self.frame.size.width? - person Jeremy Wiebe; 13.05.2014

На iOS 11 / Swift 4 ни один из приведенных выше ответов не помог мне. Я нашел несколько примеров и поработал над ними:

extension UIButton {

    func centerImageAndButton(_ gap: CGFloat, imageOnTop: Bool) {

      guard let imageView = self.currentImage,
      let titleLabel = self.titleLabel?.text else { return }

      let sign: CGFloat = imageOnTop ? 1 : -1
      self.titleEdgeInsets = UIEdgeInsetsMake((imageView.size.height + gap) * sign, -imageView.size.width, 0, 0);

      let titleSize = titleLabel.size(withAttributes:[NSAttributedStringKey.font: self.titleLabel!.font!])
      self.imageEdgeInsets = UIEdgeInsetsMake(-(titleSize.height + gap) * sign, 0, 0, -titleSize.width)
    }
}

Надеюсь, это кому-то поможет.

person Roman B    schedule 11.06.2018
comment
Спасибо, что предложил этому Роману, хотя есть проблема, когда contentEdgeInsets не включают полностью заголовок и изображение. - person Patrick; 09.08.2018

Обновлен ответ Кенни Винкера, поскольку sizeWithFont устарел в iOS 7.

-(void)layoutSubviews {
[super layoutSubviews];

int kTextTopPadding = 3;

CGRect titleLabelFrame = self.titleLabel.frame;

CGRect labelSize = [self.titleLabel.text boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, CGRectGetHeight(self.bounds)) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:self.titleLabel.font} context:nil];

CGRect imageFrame = self.imageView.frame;

CGSize fitBoxSize = (CGSize){.height = labelSize.size.height + kTextTopPadding +  imageFrame.size.height, .width = MAX(imageFrame.size.width, labelSize.size.width)};

CGRect fitBoxRect = CGRectInset(self.bounds, (self.bounds.size.width - fitBoxSize.width)/2, (self.bounds.size.height - fitBoxSize.height)/2);

imageFrame.origin.y = fitBoxRect.origin.y;
imageFrame.origin.x = CGRectGetMidX(fitBoxRect) - (imageFrame.size.width/2);
self.imageView.frame = imageFrame;

// Adjust the label size to fit the text, and move it below the image

titleLabelFrame.size.width = labelSize.size.width;
titleLabelFrame.size.height = labelSize.size.height;
titleLabelFrame.origin.x = (self.frame.size.width / 2) - (labelSize.size.width / 2);
titleLabelFrame.origin.y = fitBoxRect.origin.y + imageFrame.size.height + kTextTopPadding;
self.titleLabel.frame = titleLabelFrame;
}
person Joped    schedule 21.07.2015
comment
Поскольку предварительная версия iOS 7 становится все более и более устаревшей, это должен быть новый принятый ответ. - person Mehlyfication; 21.08.2015

Используя код Кенни Винкера и Симеона, я создаю этот быстрый код, который мне подходит.

import UIKit

@IBDesignable
class TopIconButton: UIButton {
    override func layoutSubviews() {
        super.layoutSubviews()

        let kTextTopPadding:CGFloat = 3.0;

        var titleLabelFrame = self.titleLabel!.frame;


        let labelSize = titleLabel!.sizeThatFits(CGSizeMake(CGRectGetWidth(self.contentRectForBounds(self.bounds)), CGFloat.max))

        var imageFrame = self.imageView!.frame;

        let fitBoxSize = CGSizeMake(max(imageFrame.size.width, labelSize.width), labelSize.height + kTextTopPadding + imageFrame.size.    height)

        let fitBoxRect = CGRectInset(self.bounds, (self.bounds.size.width - fitBoxSize.width)/2, (self.bounds.size.height - fitBoxSize.    height)/2);

        imageFrame.origin.y = fitBoxRect.origin.y;
        imageFrame.origin.x = CGRectGetMidX(fitBoxRect) - (imageFrame.size.width/2);
        self.imageView!.frame = imageFrame;

        // Adjust the label size to fit the text, and move it below the image

        titleLabelFrame.size.width = labelSize.width;
        titleLabelFrame.size.height = labelSize.height;
        titleLabelFrame.origin.x = (self.frame.size.width / 2) - (labelSize.width / 2);
        titleLabelFrame.origin.y = fitBoxRect.origin.y + imageFrame.size.height + kTextTopPadding;
        self.titleLabel!.frame = titleLabelFrame;
        self.titleLabel!.textAlignment = .Center
    }

}
person Marcellus S.B.    schedule 23.12.2015

Swift 5, AutoLayout

Я реализовал комбинацию нескольких подходов, и мне это лучше всего подходит. Уловка заключается в переопределении методов titleRect(forContentRect:), imageRect(forContentRect:) и intrinsicContentSize получателя вычисленными значениями этих представлений.

Результат:

введите описание изображения здесь

final class CustomButton: UIButton {

    override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
        let superRect = super.titleRect(forContentRect: contentRect)
        return CGRect(
            x: 0,
            y: contentRect.height - superRect.height,
            width: contentRect.width,
            height: superRect.height
        )
    }

    override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
        let superRect = super.imageRect(forContentRect: contentRect)
        return CGRect(
            x: contentRect.width / 2 - superRect.width / 2,
            y: (contentRect.height - titleRect(forContentRect: contentRect).height) / 2 - superRect.height / 2,
            width: superRect.width,
            height: superRect.height
        )
    }

    override var intrinsicContentSize: CGSize {
        _ = super.intrinsicContentSize
        guard let image = imageView?.image else { return super.intrinsicContentSize }
        let size = titleLabel?.sizeThatFits(contentRect(forBounds: bounds).size) ?? .zero
        let spacing: CGFloat = 12
        return CGSize(width: max(size.width, image.size.width), height: image.size.height + size.height + spacing)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    private func setup() {
        titleLabel?.textAlignment = .center
    }
}

Приведенное выше решение может не работать в iOS 12 и старше. В этом случае создайте константы с фиксированным размером изображения и высотой метки в зависимости от размера шрифта.

final class CustomButton: UIButton {

    private enum Constants {
        static let imageSize: CGFloat = 48
        static let titleHeight = yourFont.pointSize - yourFont.descender
    }

    override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
        _ = super.titleRect(forContentRect: contentRect)
        return CGRect(
            x: 0,
            y: contentRect.height - Constants.titleHeight,
            width: contentRect.width,
            height: Constants.titleHeight
        )
    }

    override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
        return CGRect(
            x: contentRect.width / 2 - Constants.imageSize / 2,
            y: (contentRect.height - titleRect(forContentRect: contentRect).height) / 2 - Constants.imageSize / 2,
            width: Constants.imageSize,
            height: Constants.imageSize
        )
    }

    override var intrinsicContentSize: CGSize {
        _ = super.intrinsicContentSize
        let size = titleLabel?.sizeThatFits(contentRect(forBounds: bounds).size) ?? .zero
        let spacing: CGFloat = 12
        return CGSize(
            width: max(size.width, Constants.imageSize),
            height: Constants.imageSize + Constants.titleHeight + spacing
        )
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    private func setup() {
        titleLabel?.textAlignment = .center
    }
}
person Robert Dresler    schedule 20.10.2020
comment
Думаю, вам откуда-то не хватает расширения style == .postEditorTypeOption. - person Peter Suwara; 28.12.2020
comment
@PeterSuwara, спасибо, вы правы. Этого просто не должно быть - person Robert Dresler; 29.12.2020

@Tiago Я меняю твой ответ вот так. Он отлично работает со всеми размерами

func alignImageAndTitleVertically(padding: CGFloat = 5.0) {
        self.sizeToFit()
        let imageSize = self.imageView!.frame.size
        let titleSize = self.titleLabel!.frame.size
        let totalHeight = imageSize.height + titleSize.height + padding

        self.imageEdgeInsets = UIEdgeInsets(
            top: -(totalHeight - imageSize.height),
            left: 0,
            bottom: 0,
            right: -titleSize.width
        )

        self.titleEdgeInsets = UIEdgeInsets(
            top: 0,
            left: 0,
            bottom: -(totalHeight - titleSize.height),
            right: titleSize.height
        )
    }
person mychar    schedule 25.06.2016

Swift 5 - метод ниже работает для меня

func centerVerticallyWithPadding(padding : CGFloat) {
        guard
            let imageViewSize = self.imageView?.frame.size,
            let titleLabelSize = self.titleLabel?.frame.size else {
            return
        }

        let totalHeight = imageViewSize.height + titleLabelSize.height + padding

        self.imageEdgeInsets = UIEdgeInsets(
            top: max(0, -(totalHeight - imageViewSize.height)),
            left: 0.0,
            bottom: 0.0,
            right: -titleLabelSize.width
        )

        self.titleEdgeInsets = UIEdgeInsets(
            top: (totalHeight - imageViewSize.height),
            left: -imageViewSize.width,
            bottom: -(totalHeight - titleLabelSize.height),
            right: 0.0
        )

        self.contentEdgeInsets = UIEdgeInsets(
            top: 0.0,
            left: 0.0,
            bottom: titleLabelSize.height,
            right: 0.0
        )
    }

Убедитесь, что заголовок вашей кнопки не усечен в раскадровке / xib, иначе выберите
Решение 2

class SVVerticalButton: UIButton {

    override func layoutSubviews() {
        super.layoutSubviews()
        let padding : CGFloat = 2.0
        if let imageView = self.imageView {
            imageView.frame.origin.x = (self.bounds.size.width - imageView.frame.size.width) / 2.0
            imageView.frame.origin.y = max(0,(self.bounds.size.height - (imageView.frame.size.height + (titleLabel?.frame.size.height ?? 0.0) + padding)) / 2.0)
        }
        if let titleLabel = self.titleLabel {
            titleLabel.frame.origin.x = 0
            titleLabel.frame.origin.y = self.bounds.size.height - titleLabel.frame.size.height
            titleLabel.frame.size.width = self.bounds.size.width
            titleLabel.textAlignment = .center
        }
    }

}
person Shreyank    schedule 09.01.2020
comment
Верх по-прежнему срезан ???? у этого раствора размер меньше ожидаемого. - person Zorayr; 18.08.2020

Вам просто нужно отрегулировать все три краевых вставки в зависимости от размера вашего изображения и заголовка:

button.contentEdgeInsets = UIEdgeInsetsMake(0, 0, titleLabelBounds.height + 4, 0)
button.titleEdgeInsets = UIEdgeInsetsMake(image.size.height + 8, -image.size.width, 0, 0)
button.imageEdgeInsets = UIEdgeInsetsMake(0, 0, 0, -titleLabelBounds.width)

Вы можете получить границы метки заголовка, вызвав sizeToFit после установки его текста. Горизонтальный интервал должен работать независимо от размера текста, шрифта и изображения, но я не знаю единого решения для согласования вертикального интервала и вставки нижнего края содержимого.

person Justin Driscoll    schedule 29.05.2015

Вот ответ "Bear With Me" как подкласс в Swift 2.0. Чтобы использовать его, просто измените класс кнопки в Interface Builder на VerticalButton, и он волшебным образом обновит предварительный просмотр.

Я также обновил его, чтобы рассчитать правильный размер внутреннего содержимого для автоматического размещения.

import UIKit

@IBDesignable

class VerticalButton: UIButton {
    @IBInspectable var padding: CGFloat = 8

    override func prepareForInterfaceBuilder() {
        super.prepareForInterfaceBuilder()

        update()
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        update()
    }

    func update() {
        let imageBounds = self.imageView!.bounds
        let titleBounds = self.titleLabel!.bounds
        let totalHeight = CGRectGetHeight(imageBounds) + padding + CGRectGetHeight(titleBounds)

        self.imageEdgeInsets = UIEdgeInsets(
            top: -(totalHeight - CGRectGetHeight(imageBounds)),
            left: 0,
            bottom: 0,
            right: -CGRectGetWidth(titleBounds)
        )

        self.titleEdgeInsets = UIEdgeInsets(
            top: 0,
            left: -CGRectGetWidth(imageBounds),
            bottom: -(totalHeight - CGRectGetHeight(titleBounds)),
            right: 0
        )
    }

    override func intrinsicContentSize() -> CGSize {
        let imageBounds = self.imageView!.bounds
        let titleBounds = self.titleLabel!.bounds

        let width = CGRectGetWidth(imageBounds) > CGRectGetWidth(titleBounds) ? CGRectGetWidth(imageBounds) : CGRectGetWidth(titleBounds)
        let height = CGRectGetHeight(imageBounds) + padding + CGRectGetHeight(titleBounds)

        return CGSizeMake(width, height)
    }
}
person Maciej Swic    schedule 03.03.2016
comment
Заканчивается бесконечным циклом, в котором layoutSubviews() вызывается повторно в моем случае: intrinsicContentSize обращается к imageView, что вызывает вызов layoutSubviews, который обращается к imageView и т. Д. - person ctietze; 16.05.2016

Я взял комбинацию ответов здесь и придумал тот, который, кажется, работает для меня в Swift. Мне не нравится, как я просто перекрыл вставки, но это работает. Я буду открыт для предложений по улучшению в комментариях. Вроде корректно работает с sizeToFit() и с автоматическим макетом.

import UIKit

/// A button that displays an image centered above the title.  This implementation 
/// only works when both an image and title are set, and ignores
/// any changes you make to edge insets.
class CenteredButton: UIButton
{
    let padding: CGFloat = 0.0

    override func layoutSubviews() {
        if imageView?.image != nil && titleLabel?.text != nil {
            let imageSize: CGSize = imageView!.image!.size
            titleEdgeInsets = UIEdgeInsetsMake(0.0, -imageSize.width, -(imageSize.height + padding), 0.0)
            let labelString = NSString(string: titleLabel!.text!)
            let titleSize = labelString.sizeWithAttributes([NSFontAttributeName: titleLabel!.font])
            imageEdgeInsets = UIEdgeInsetsMake(-(titleSize.height + padding), 0.0, 0.0, -titleSize.width)
            let edgeOffset = abs(titleSize.height - imageSize.height) / 2.0;
            contentEdgeInsets = UIEdgeInsetsMake(edgeOffset, 0.0, edgeOffset, 0.0)
        }
        super.layoutSubviews()
    }

    override func sizeThatFits(size: CGSize) -> CGSize {
        let defaultSize = super.sizeThatFits(size)
        if let titleSize = titleLabel?.sizeThatFits(size),
        let imageSize = imageView?.sizeThatFits(size) {
            return CGSize(width: ceil(max(imageSize.width, titleSize.width)), height: ceil(imageSize.height + titleSize.height + padding))
        }
        return defaultSize
    }

    override func intrinsicContentSize() -> CGSize {
        let size = sizeThatFits(CGSize(width: CGFloat.max, height: CGFloat.max))
        return size
    }
}
person icecrystal23    schedule 06.08.2016

Используйте эти два метода:

func titleRect(forContentRect contentRect: CGRect) -> CGRect
func imageRect(forContentRect contentRect: CGRect) -> CGRect

Пример:

class VerticalButton: UIButton {

  override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
    let titleRect = super.titleRect(forContentRect: contentRect)
    let imageRect = super.imageRect(forContentRect: contentRect)

    return CGRect(x: 0,
                  y: contentRect.height - (contentRect.height - padding - imageRect.size.height - titleRect.size.height) / 2 - titleRect.size.height,
                  width: contentRect.width,
                  height: titleRect.height)
  }

  override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
    let imageRect = super.imageRect(forContentRect: contentRect)
    let titleRect = self.titleRect(forContentRect: contentRect)

    return CGRect(x: contentRect.width/2.0 - imageRect.width/2.0,
                  y: (contentRect.height - padding - imageRect.size.height - titleRect.size.height) / 2,
                  width: imageRect.width,
                  height: imageRect.height)
  }

  private let padding: CGFloat
  init(padding: CGFloat) {
    self.padding = padding

    super.init(frame: .zero)
    self.titleLabel?.textAlignment = .center
  }

  required init?(coder aDecoder: NSCoder) { fatalError() }
}

extension UIButton {

  static func vertical(padding: CGFloat) -> UIButton {
    return VerticalButton(padding: padding)
  }
}

И вы можете использовать:

let myButton = UIButton.vertical(padding: 6)
person ober    schedule 10.07.2018

Удобное решение для локализации:

Так много отличных решений, ребята, но я хотел бы добавить сюда примечание для тех, кто использует локализацию.

Вам нужно поменять местами левое и правое значения EdgeInstets, чтобы кнопка располагалась правильно в случае изменения языкового направления с LtR на RtL.

Используя аналогичное решение, я бы реализовал его следующим образом:

extension UIButton {

    func alignVertical(spacing: CGFloat, lang: String) {
        guard let imageSize = self.imageView?.image?.size,
            let text = self.titleLabel?.text,
            let font = self.titleLabel?.font
        else { return }

        let labelString = NSString(string: text)
        let titleSize = labelString.size(
            withAttributes: [NSAttributedString.Key.font: font]
        )

        var titleLeftInset: CGFloat = -imageSize.width
        var titleRigtInset: CGFloat = 0.0

        var imageLeftInset: CGFloat = 0.0
        var imageRightInset: CGFloat = -titleSize.width

        if Locale.current.languageCode! != "en" { // If not Left to Right language
            titleLeftInset = 0.0
            titleRigtInset = -imageSize.width

            imageLeftInset = -titleSize.width
            imageRightInset = 0.0
        }

        self.titleEdgeInsets = UIEdgeInsets(
            top: 0.0,
            left: titleLeftInset,
            bottom: -(imageSize.height + spacing),
            right: titleRigtInset
        )
        self.imageEdgeInsets = UIEdgeInsets(
            top: -(titleSize.height + spacing),
            left: imageLeftInset,
            bottom: 0.0,
            right: imageRightInset
        )
        let edgeOffset = abs(titleSize.height - imageSize.height) / 2.0;
        self.contentEdgeInsets = UIEdgeInsets(
            top: edgeOffset,
            left: 0.0,
            bottom: edgeOffset,
            right: 0.0
        )
    }
}

person Wissa    schedule 17.04.2019
comment
Есть много неанглийских языков LTR. Лучше проверять EffectiveUserInterfaceLayoutDirection на кнопке. - person Alexsander Akers; 09.07.2019
comment
С большим заголовком он создает проблему. Любое решение для этого? - person Maulik Pandya; 08.01.2021

Это определенно излишек для этого вопроса, однако ... В одном из моих проектов мне сначала пришлось реализовать кнопку с иконкой, выровненной по левому краю. Затем у нас появилась еще одна кнопка с заголовком под изображением. Я искал существующее решение, но безуспешно Итак, вот кнопка выравнивания:

@IBDesignable
class AlignableButton: UIButton {

override class var requiresConstraintBasedLayout: Bool {
    return true
}

@objc enum IconAlignment: Int {
    case top, left, right, bottom
}

// MARK: - Designables
@IBInspectable var iconAlignmentValue: Int {
    set {
        iconAlignment = IconAlignment(rawValue: newValue) ?? .left
    }
    get {
        return iconAlignment.rawValue
    }
}

var iconAlignment: IconAlignment = .left

@IBInspectable var titleAlignmentValue: Int {
    set {
        titleAlignment = NSTextAlignment(rawValue: newValue) ?? .left
    }
    get {
        return titleAlignment.rawValue
    }
}

var titleAlignment: NSTextAlignment = .left

// MARK: - Corner Radius
@IBInspectable
var cornerRadius: CGFloat {
    get {
        return layer.cornerRadius
    }
    set {
        layer.masksToBounds = (newValue != 0)
        layer.cornerRadius = newValue
    }
}

// MARK: - Content size
override var intrinsicContentSize: CGSize {
    
    switch iconAlignment {
    case .top, .bottom:
        return verticalAlignedIntrinsicContentSize
    
    default:
        return super.intrinsicContentSize
    }
}

private var verticalAlignedIntrinsicContentSize: CGSize {
    
    if let imageSize = imageView?.intrinsicContentSize,
        let labelSize = titleLabel?.intrinsicContentSize {
        
        let width = max(imageSize.width, labelSize.width) + contentEdgeInsets.left + contentEdgeInsets.right
        let height = imageSize.height + labelSize.height + contentEdgeInsets.top + contentEdgeInsets.bottom
        
        return CGSize(
            width: ceil(width),
            height: ceil(height)
        )
    }
    
    return super.intrinsicContentSize
}

// MARK: - Image Rect
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
    
    switch iconAlignment {
    case .top:
        return topAlignedImageRect(forContentRect: contentRect)
    case .bottom:
        return bottomAlignedImageRect(forContentRect: contentRect)
    case .left:
        return leftAlignedImageRect(forContentRect: contentRect)
    case .right:
        return rightAlignedImageRect(forContentRect: contentRect)
    }
}

func topAlignedImageRect(forContentRect contentRect: CGRect) -> CGRect {
    let rect = super.imageRect(forContentRect: contentRect)
    
    let x = (contentRect.width - rect.width) / 2.0 + contentRect.minX
    let y = contentRect.minY
    let w = rect.width
    let h = rect.height
    
    return CGRect(
        x: x,
        y: y,
        width: w,
        height: h
    ).inset(by: imageEdgeInsets)
}

func bottomAlignedImageRect(forContentRect contentRect: CGRect) -> CGRect {
    let rect = super.imageRect(forContentRect: contentRect)
    
    let x = (contentRect.width - rect.width) / 2.0 + contentRect.minX
    let y = contentRect.height - rect.height + contentRect.minY
    let w = rect.width
    let h = rect.height
    
    return CGRect(
        x: x,
        y: y,
        width: w,
        height: h
    ).inset(by: imageEdgeInsets)
}

func leftAlignedImageRect(forContentRect contentRect: CGRect) -> CGRect {
    let rect = super.imageRect(forContentRect: contentRect)
    
    let x = contentRect.minX
    let y = (contentRect.height - rect.height) / 2 + contentRect.minY
    let w = rect.width
    let h = rect.height
    
    return CGRect(
        x: x,
        y: y,
        width: w,
        height: h
    ).inset(by: imageEdgeInsets)
}

func rightAlignedImageRect(forContentRect contentRect: CGRect) -> CGRect {
    let rect = super.imageRect(forContentRect: contentRect)
    
    let x = (contentRect.width - rect.width) + contentRect.minX
    let y = (contentRect.height - rect.height) / 2 + contentRect.minY
    let w = rect.width
    let h = rect.height
    
    return CGRect(
        x: x,
        y: y,
        width: w,
        height: h
    ).inset(by: imageEdgeInsets)
}

// MARK: - Title Rect
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
    
    switch iconAlignment {
    case .top:
        return topAlignedTitleRect(forContentRect: contentRect)
    case .bottom:
        return bottomAlignedTitleRect(forContentRect: contentRect)
    case .left:
        return leftAlignedTitleRect(forContentRect: contentRect)
    case .right:
        return rightAlignedTitleRect(forContentRect: contentRect)
    }
}

func topAlignedTitleRect(forContentRect contentRect: CGRect) -> CGRect {
    
    let rect = super.titleRect(forContentRect: contentRect)

    let x = contentRect.minX
    let y = contentRect.height - rect.height + contentRect.minY
    let w = contentRect.width
    let h = rect.height
    
    return CGRect(
        x: x,
        y: y,
        width: w,
        height: h
    ).inset(by: titleEdgeInsets)
}

func bottomAlignedTitleRect(forContentRect contentRect: CGRect) -> CGRect {
    
    let rect = super.titleRect(forContentRect: contentRect)
    
    let x = contentRect.minX
    let y = contentRect.minY
    let w = contentRect.width
    let h = rect.height
    
    return CGRect(
        x: x,
        y: y,
        width: w,
        height: h
    ).inset(by: titleEdgeInsets)
}

func leftAlignedTitleRect(forContentRect contentRect: CGRect) -> CGRect {
    
    let titleRect = super.titleRect(forContentRect: contentRect)
    let imageRect = self.imageRect(forContentRect: contentRect)
    
    let x = imageRect.width + imageRect.minX
    let y = (contentRect.height - titleRect.height) / 2.0 + contentRect.minY
    let w = contentRect.width - imageRect.width * 2.0
    let h = titleRect.height
    
    return CGRect(
        x: x,
        y: y,
        width: w,
        height: h
    ).inset(by: titleEdgeInsets)
}

func rightAlignedTitleRect(forContentRect contentRect: CGRect) -> CGRect {
    
    let titleRect = super.titleRect(forContentRect: contentRect)
    let imageRect = self.imageRect(forContentRect: contentRect)

    let x = contentRect.minX + imageRect.width
    let y = (contentRect.height - titleRect.height) / 2.0 + contentRect.minY
    let w = contentRect.width - imageRect.width * 2.0
    let h = titleRect.height
    
    return CGRect(
        x: x,
        y: y,
        width: w,
        height: h
    ).inset(by: titleEdgeInsets)
}

// MARK: - Lifecycle
override func awakeFromNib() {
    super.awakeFromNib()
    
    titleLabel?.textAlignment = titleAlignment
}

override func prepareForInterfaceBuilder() {
    super.prepareForInterfaceBuilder()
    
    titleLabel?.textAlignment = titleAlignment
}
}

Надеюсь, вы найдете ее полезной.

person eagle.dan.1349    schedule 23.06.2020

Я думаю, что один из лучших способов сделать это - создать подкласс UIButton и переопределить некоторые методы рендеринга:

- (void)awakeFromNib
{
    [super awakeFromNib];
    [self setupSubViews];
}

- (instancetype)init
{
    if (self = [super init])
    {
        [self setupSubViews];
    }
    return self;
}

- (void)setupSubViews
{
    [self.imageView setTranslatesAutoresizingMaskIntoConstraints:NO];
    [self addConstraint:[NSLayoutConstraint constraintWithItem:self attribute:NSLayoutAttributeCenterX relatedBy:NSLayoutRelationEqual toItem:self.imageView attribute:NSLayoutAttributeCenterX multiplier:1 constant:0]];
    [self.titleLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
    [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[imageView][titleLabel]|" options:NSLayoutFormatAlignAllCenterX metrics:nil views:@{@"imageView": self.imageView, @"titleLabel": self.titleLabel}]];
}

- (CGSize)intrinsicContentSize
{
    CGSize imageSize = self.imageView.image.size;
    CGSize titleSize = [self.titleLabel.text sizeWithAttributes:@{NSFontAttributeName: self.titleLabel.font}];
    return CGSizeMake(MAX(imageSize.width, titleSize.width), imageSize.height + titleSize.height);
}
person Basem Saadawy    schedule 16.12.2014

Я обнаружил, что ответ Симеона был, вероятно, лучшим, но он давал мне странные результаты на некоторых кнопках, и я просто не мог работать почему. Поэтому, используя его ответ в качестве основы, я реализовал свои кнопки, как показано ниже:

#define PADDING 2.0f

@implementation OOButtonVerticalImageText

-(CGSize) intrinsicContentSize {
  CGSize size = [super intrinsicContentSize];
  CGFloat labelHeight = 0.0f;
  CGSize titleSize = [self.titleLabel sizeThatFits:CGSizeMake([self contentRectForBounds:self.bounds].size.width, CGFLOAT_MAX)];
  labelHeight = titleSize.height;
  return CGSizeMake(MAX(titleSize.width, self.imageView.image.size.width), self.imageView.image.size.height + labelHeight + PADDING);
}

-(void) layoutSubviews {
  [super layoutSubviews];

  CGSize titleSize = [self.titleLabel sizeThatFits:CGSizeMake([self contentRectForBounds:self.bounds].size.width, CGFLOAT_MAX)];
  self.titleLabel.frame = CGRectMake((self.bounds.size.width - titleSize.width)/2.0f,
                                     self.bounds.size.height - titleSize.height - PADDING,
                                     titleSize.width,
                                     titleSize.height);

  CGSize ivSize = self.imageView.frame.size;
  self.imageView.frame = CGRectMake((self.bounds.size.width - ivSize.width)/2.0f,
                                    self.titleLabel.frame.origin.y - ivSize.height - PADDING,
                                    ivSize.width,
                                    ivSize.height);
}

@end
person Mof    schedule 05.01.2018

Вот мой подкласс UIButton, который решает эту проблему:

@implementation MyVerticalButton

@synthesize titleAtBottom; // BOOL property

- (id)initWithFrame:(CGRect)frame
{
  self = [super initWithFrame:frame];
  if (self) {
    self.titleAtBottom = YES;
  }
  return self;
}

- (CGSize)sizeThatFits:(CGSize)size {
  self.titleLabel.text = [self titleForState: self.state];

  UIEdgeInsets imageInsets = self.imageEdgeInsets;
  UIEdgeInsets titleInsets = self.titleEdgeInsets;

  CGSize imageSize = [self imageForState: self.state].size;
  if (!CGSizeEqualToSize(imageSize, CGSizeZero)) {
    imageSize.width += imageInsets.left + imageInsets.right;
    imageSize.height += imageInsets.top + imageInsets.bottom;

  }

  CGSize textSize = [self.titleLabel sizeThatFits: CGSizeMake(size.width - titleInsets.left - titleInsets.right,
                                                              size.height -(imageSize.width +
                                                                            titleInsets.top+titleInsets.bottom))];
  if (!CGSizeEqualToSize(textSize, CGSizeZero)) {
    textSize.width += titleInsets.left + titleInsets.right;
    textSize.height += titleInsets.top + titleInsets.bottom;
  }

  CGSize result = CGSizeMake(MAX(textSize.width, imageSize.width),
                             textSize.height + imageSize.height);
  return result;
}

- (void)layoutSubviews {
  // needed to update all properities of child views:
  [super layoutSubviews];

  CGRect bounds = self.bounds;

  CGRect titleFrame = UIEdgeInsetsInsetRect(bounds, self.titleEdgeInsets);
  CGRect imageFrame = UIEdgeInsetsInsetRect(bounds, self.imageEdgeInsets);
  if (self.titleAtBottom) {
    CGFloat titleHeight = [self.titleLabel sizeThatFits: titleFrame.size].height;
    titleFrame.origin.y = CGRectGetMaxY(titleFrame)-titleHeight;
    titleFrame.size.height = titleHeight;
    titleFrame = CGRectStandardize(titleFrame);
    self.titleLabel.frame = titleFrame;

    CGFloat imageBottom = CGRectGetMinY(titleFrame)-(self.titleEdgeInsets.top+self.imageEdgeInsets.bottom);
    imageFrame.size.height = imageBottom - CGRectGetMinY(imageFrame);
    self.imageView.frame = CGRectStandardize(imageFrame);
  } else {
    CGFloat titleHeight = [self.titleLabel sizeThatFits: titleFrame.size].height;
    titleFrame.size.height = titleHeight;
    titleFrame = CGRectStandardize(titleFrame);
    self.titleLabel.frame = titleFrame;

    CGFloat imageTop = CGRectGetMaxY(titleFrame)+(self.titleEdgeInsets.bottom+self.imageEdgeInsets.top);
    imageFrame.size.height = CGRectGetMaxY(imageFrame) - imageTop;
    self.imageView.frame = CGRectStandardize(imageFrame);
  }
}

- (void)setTitleAtBottom:(BOOL)newTitleAtBottom {
  if (titleAtBottom!=newTitleAtBottom) {
    titleAtBottom=newTitleAtBottom;
    [self setNeedsLayout];
  }
}

@end

Вот и все. Работает как шарм. Проблема может возникнуть, если кнопка будет слишком маленькой, чтобы соответствовать заголовку и тексту.

person Marek R    schedule 31.07.2014
comment
Это действительно работает как шарм! И с автоматической раскладкой тоже. Большое спасибо за то, что поделились этим решением. Я сходил с ума от этого и прибегал к созданию собственного подкласса UIControl. - person valeCocoa; 25.09.2017

решение @ simeon в Objective-C

#import "CenteredButton.h"

@implementation CenteredButton

- (CGRect)titleRectForContentRect:(CGRect)contentRect
{
    CGRect rect = [super titleRectForContentRect: contentRect];
    return CGRectMake(0,
                      contentRect.size.height - rect.size.height - 5,
                      contentRect.size.width,
                      rect.size.height);
}

- (CGRect)imageRectForContentRect:(CGRect)contentRect
{
    CGRect rect = [super imageRectForContentRect: contentRect];
    CGRect titleRect = [self titleRectForContentRect: contentRect];

    return CGRectMake(contentRect.size.width / 2.0 - rect.size.width / 2.0,
                      (contentRect.size.height - titleRect.size.height)/2.0 - rect.size.height/2.0,
                      rect.size.width,
                      rect.size.height);
}

- (CGSize)intrinsicContentSize {
    CGSize imageSize = [super intrinsicContentSize];

    if (self.imageView.image) {
        UIImage* image = self.imageView.image;
        CGFloat labelHeight = 0.0;

        CGSize labelSize = [self.titleLabel sizeThatFits: CGSizeMake([self contentRectForBounds: self.bounds].size.width, CGFLOAT_MAX)];
        if (CGSizeEqualToSize(imageSize, labelSize)) {
            labelHeight = imageSize.height;
        }

        return CGSizeMake(MAX(labelSize.width, imageSize.width), image.size.height + labelHeight + 5);
    }

    return imageSize;
}

- (id) initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
     if (self) {
         [self centerTitleLabel];
     }
    return self;

}

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self centerTitleLabel];
    }
    return self;
}

- (void)centerTitleLabel {
    self.titleLabel.textAlignment = NSTextAlignmentCenter;
}

@end
person Will    schedule 29.05.2017
comment
Я думаю, что intrinsicContentSize здесь неверен. Я не понимаю, для чего предназначена часть с CGSizeEqualToSize, но у вас есть размер метки ›0, только если размер метки совпадает с intrinsicContentSize UILabel. Достаточно просто вернуть CGSizeMake(MAX(labelSize.width, image.size.width), image.size.height + labelSize.height + 5.0) в случае, если - person Oliver; 28.06.2017

Если вы используете пользовательские шрифты, расчет размера titleLabel не будет работать должным образом, вам следует заменить его на

let titleLabelSize = self.titleLabel?.text?.size(withAttributes: [NSAttributedStringKey.font: self.titleLabel!.font!])

person Benjamin Jimenez    schedule 12.06.2018

Вместо того, чтобы идти через ад, пытаясь расположить значок и текст со вставками по краям, вы можете создать NSAttributedString с вашим изображением в качестве вложения и вместо этого установить его для атрибутивного заголовка вашей кнопки:

let titleText = NSAttributedString(string: yourTitle, attributes: attributes)
let imageAttachment = NSTextAttachment()
imageAttachment.image = yourImage

let title = NSMutableAttributedString(string: "")
title.append(NSAttributedString(attachment: imageAttachment))
title.append(titleText)

button.setAttributedTitle(title, for: .normal)
person swearwolf    schedule 31.08.2018
comment
Не работает для вопроса OP, где текст должен быть по центру ниже изображения. Макеты текстового поля UIButton для отображения только одной строки, поэтому он не работает даже при использовании разрыва строки в строке с атрибутами. В противном случае было бы неплохим решением. - person Manuel; 27.11.2018
comment
Также важно установить button.titleLabel?.numberOfLines, чтобы получить необходимое количество строк. - person swearwolf; 28.11.2018

Что-то вроде этого внутри подкласса UIButton

public override func layoutSubviews() {
    super.layoutSubviews()

    imageEdgeInsets = UIEdgeInsetsMake(-10, 0, 0, 0)
    titleEdgeInsets = UIEdgeInsetsMake(0, -bounds.size.width/2 - 10, -30, 0)
}
person onmyway133    schedule 26.04.2016

iOS 11 - Objective-C

-(void)layoutSubviews {
[super layoutSubviews];

CGRect titleLabelFrame = self.titleLabel.frame;
CGSize labelSize = [self.titleLabel.text sizeWithAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:12.0f]}];
CGSize adjustedLabelSize = CGSizeMake(ceilf(labelSize.width), ceilf(labelSize.height));

CGRect imageFrame = self.imageView.frame;

CGSize fitBoxSize = (CGSize){.height = adjustedLabelSize.height + kTextTopPadding +  imageFrame.size.height, .width = MAX(imageFrame.size.width, adjustedLabelSize.width)};

CGRect fitBoxRect = CGRectInset(self.bounds, (self.bounds.size.width - fitBoxSize.width)/2, (self.bounds.size.height - fitBoxSize.height)/2);

imageFrame.origin.y = fitBoxRect.origin.y;
imageFrame.origin.x = CGRectGetMidX(fitBoxRect) - (imageFrame.size.width/2);
self.imageView.frame = imageFrame;

// Adjust the label size to fit the text, and move it below the image

titleLabelFrame.size.width = adjustedLabelSize.width;
titleLabelFrame.size.height = adjustedLabelSize.height;
titleLabelFrame.origin.x = (self.frame.size.width / 2) - (adjustedLabelSize.width / 2);
titleLabelFrame.origin.y = fitBoxRect.origin.y + imageFrame.size.height + kTextTopPadding;
self.titleLabel.frame = titleLabelFrame; }
person Brandon Stillitano    schedule 04.01.2018

Верхнее изображение и нижняя кнопка заголовка с подклассом UIButton

class VerticalButton: UIButton {
  override func layoutSubviews() {
    super.layoutSubviews()
    let padding: CGFloat = 8
    let iH = imageView?.frame.height ?? 0
    let tH = titleLabel?.frame.height ?? 0
    let v: CGFloat = (frame.height - iH - tH - padding) / 2
    if let iv = imageView {
      let x = (frame.width - iv.frame.width) / 2
      iv.frame.origin.y = v
      iv.frame.origin.x = x
    }

    if let tl = titleLabel {
      let x = (frame.width - tl.frame.width) / 2
      tl.frame.origin.y = frame.height - tl.frame.height - v
      tl.frame.origin.x = x
    }
  }
}
person Shohin    schedule 05.05.2020

Другой способ, который я нашел:

UIButton имеет 2 UIImageViews. Первого из них мы можем достичь с помощью свойства imageView UIButton. Другое представление UIImage недоступно через свойства UIButton, но этот другой UIImageView идеально подходит для реализации центрированной кнопки. Чтобы получить доступ к просмотру фонового изображения, я добавил расширение:

extension UIButton {

    var backgroundImageView: UIImageView? {
        return subviews.first(where: { $0.isKind(of: UIImageView.self) && $0 != imageView }) as? UIImageView
    }
}

Следующим шагом будет настройка кнопки:

centeredButton.setBackgroundImage(perfectImage, for: .normal) // This sets image to Button's backgroundImageView
centeredButton.backgroundImageView?.contentMode = .top // This moves image to the top of the Button's frame
centeredButton.contentVerticalAlignment = .bottom // This moves label to the bottom of the Button's frame.

Следующим шагом является установка отступов между изображением и меткой. Вы можете сделать это, добавив к кнопке ограничение по высоте. Изображение всегда будет вверху, подпись внизу.

person Dmitry    schedule 01.04.2021

Это довольно просто.

Вместо этого:

   button.setImage(UIImage(named: "image"), forState: .Normal)

Использовать это:

   button.setBackgroundImage(UIImage(named: "image", forState: .Normal)

Затем вы можете легко добавить текст на кнопку, используя:

// button.titleLabel! .font = UIFont (name: "FontName", размер: 30)

 button.setTitle("TitleText", forState: UIControlState.Normal)
person kumarsiddharth123    schedule 28.07.2016