Autolayout - o tamanho intrínseco do UIButton não inclui inserções de título

196

Se eu tiver um UIButton organizado usando o autolayout, seu tamanho será ajustado para caber em seu conteúdo.

Se eu definir uma imagem como button.image, o tamanho do instrinsic novamente parece explicar isso.

No entanto, se eu ajustar o titleEdgeInsetsbotão, o layout não será responsável por isso e truncará o título do botão.

Como garantir que a largura intrínseca do botão seja responsável pela inserção?

insira a descrição da imagem aqui

Editar:

Estou usando o seguinte:

[self.backButton setTitleEdgeInsets:UIEdgeInsetsMake(0, 5, 0, 0)];

O objetivo é adicionar alguma separação entre a imagem e o texto.

Ben Packard
fonte
3
Você registrou isso como um radar? Certamente parece ser um erro nos cálculos intrínsecos de tamanho do UIButton.
Ryan Poolos
1
Eu estava pronto para arquivar um radar, mas esse parece realmente ser um comportamento esperado. Isso está documentado nas propriedades * EdgeInsets do UIButton : "As inserções que você especificar são aplicadas ao retângulo do título depois que o retângulo tiver sido dimensionado para caber no texto do botão. Portanto, valores de inserção positivos podem realmente recortar o texto do título. [...] O botão não usa essa propriedade para determinar intrinsicContentSize e sizeThatFits :. "
Guillaume Algis
7
@GuillaumeAlgis Eu diria que, embora este é um comportamento declarado, é não em tudo o que se espera que aconteça quando se utiliza autolayout. Eu arquivei um bug e incentivaria outras pessoas a arquivar um também.
Memórias 14/10/2014
Se você pode vincular o bug do radar aqui, podemos clicar nele e marcar com +1?
Gprasant #
1
de titleEdgeInsetdocumentação: The insets you specify are applied to the title rectangle after that rectangle has been sized to fit the button’s text. Thus, positive inset values may actually clip the title text. Então, adicionando inserção você está forçando o botão para cortar o texto, com certeza
Marco Pappalardo

Respostas:

192

Você pode resolver isso sem precisar substituir nenhum método ou definir uma restrição de largura arbitrária. Você pode fazer tudo isso no Interface Builder da seguinte maneira.

  • A largura intrínseca do botão é derivada da largura do título mais da largura do ícone e das inserções de borda de conteúdo esquerda e direita .

  • Se um botão tiver uma imagem e um texto, eles serão centralizados como um grupo, sem preenchimento entre eles.

  • Se você adicionar um conteúdo à esquerda, ele será calculado em relação ao texto, não ao texto + ícone.

  • Se você definir uma imagem negativa esquerda inserida, a imagem será puxada para a esquerda, mas a largura total do botão não será afetada.

  • Se você definir uma imagem esquerda negativa inserida, o layout real utilizará metade desse valor. Portanto, para obter uma inserção esquerda de -20 pontos, você deve usar um valor de inserção esquerda de -40 pontos no Interface Builder.

Portanto, você fornece uma inserção de conteúdo esquerda grande o suficiente para criar espaço para a inserção esquerda desejada e o preenchimento interno entre o ícone e o texto e, em seguida, muda o ícone para a esquerda dobrando a quantidade de preenchimento desejada entre o ícone e o texto. O resultado é um botão com inserções de conteúdo iguais à esquerda e à direita e um par de texto e ícone centralizado como um grupo, com uma quantidade específica de preenchimento entre eles.

Alguns exemplos de valores:

// Produces a button with the layout:
// |-20-icon-10-text-20-|
// AutoLayout intrinsic width works as you'd desire.
button.contentEdgeInsets = UIEdgeInsetsMake(10, 30, 10, 20)
button.imageEdgeInsets = UIEdgeInsetsMake(0, -20, 0, 0)
jaredsinclair
fonte
por que o layout real usa metade do valor de inserção esquerdo negativo? Eu encontrei o mesmo problema!
21417 Tony
1
É ótimo que haja uma solução alternativa, mas espero que isso não seja usado para justificar o comportamento estranho de UIButton.
funct7
205

Você pode fazer isso funcionar no Interface Builder (sem escrever nenhum código), usando uma combinação de títulos e inserções de conteúdo negativos e positivos.

insira a descrição da imagem aqui

Atualização : O Xcode 7 possui um erro no qual você não pode inserir valores negativos no Rightcampo Inset, mas pode usar o controle stepper ao lado para diminuir o valor. (Obrigado Stuart)

Isso adicionará 8 pontos de espaçamento entre a imagem e o título e aumentará a largura intrínseca do botão na mesma quantidade. Como isso:

insira a descrição da imagem aqui

n.Drake
fonte
2
Ele está usando o contentEdgeInsets (que não é um buggy) para permitir o layout automático para aumentar a largura do botão. E mova o rótulo para o espaço vazio à direita. Maneira inteligente de solucionar o bug inserido na borda do título.
Ugur
7
Esse truque não funciona mais. O construtor de interface não aceita mais valores negativos no Rightcampo.
Joris Mans
7
@JorisMans Você não pode digitar valores negativos, mas funcionou para mim usando o controle stepper à direita do campo de texto para diminuir o valor negativo necessário ... vai entender!
Stuart
3
Esta deve ser a primeira resposta, por que está aqui embaixo? Eu tentei os outros 5 antes de encontrar este ...
Senhor Zsolt
2
Fiz Direito de inserção de conteúdo 16 para centralizar o texto em UIButton
coolcool1994
96

Por que não substituir o intrinsicContentSizemétodo no UIView? Por exemplo:

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

    return CGSizeMake(s.width + self.titleEdgeInsets.left + self.titleEdgeInsets.right,
                      s.height + self.titleEdgeInsets.top + self.titleEdgeInsets.bottom);
}

Isso deve informar ao sistema de pagamento automático que deve aumentar o tamanho do botão para permitir inserções e mostrar o texto completo. Não estou no meu próprio computador, então não testei isso.

Maarten
fonte
1
Os botões não devem ser substituídos, tanto quanto eu sei. O problema é que cada tipo de botão é implementado por uma subclasse diferente.
Sulthan 23/07
2
intrinsicContentSizeé um método no UIView, não no UIButton, portanto você não estaria mexendo em nenhum método UIButton. A Apple não acredita que seja um problema: "A substituição deste método permite que uma exibição personalizada comunique ao sistema de layout qual tamanho ele gostaria de basear em seu conteúdo". E o OP não disse nada sobre botões diferentes, apenas um.
Maarten
1
Definitivamente, isso funciona e é a solução que eu usei. intrinsicContentSizeé de fato um método no UIView e UIButton é uma subclasse do UIView; portanto, é possível substituir esse método; nada nos documentos da Apple diz que você não deveria. Basta criar uma subclasse UIButton usando o método substituído de Maarten e alterar seu UIButton no Interface Builder para ser do tipo YourUIButtonSubclass e ele funcionará perfeitamente.
n8tr 24/01
37
Parece-me intrinsicContentSizeque o UIButton deve adicionar o títuloEdgeInsets, vou arquivar um bug na Apple.
progrmr
6
Eu concordo, e o mesmo para imageEdgeInsets.
Ricardo Sanchez-Saez
87

Você não especificou como está definindo as inserções, portanto, acho que você está usando titleEdgeInsets porque vejo o mesmo efeito que você está obtendo. Se eu usar contentEdgeInsets, ele funcionará corretamente.

- (IBAction)ChangeTitle:(UIButton *)sender {
    self.button.contentEdgeInsets = UIEdgeInsetsMake(0,20,0,20);
    [self.button setTitle:@"Long Long Title" forState:UIControlStateNormal];
}
rdelmar
fonte
Na verdade, estou usando titleEdgeInsets. Preciso distanciar o título da imagem, não a imagem da borda do botão. Talvez eu deva usar apenas uma imagem com algum preenchimento? Parece hacky embora.
Ben Packard
Isso funciona perfeitamente em combinação com o autolayout, obrigado!
Cal S
3
Esta é a melhor solução, pois faz exatamente o que você deseja, sem tocar no intrinsicContentSize.
RyJ
28
Isso NÃO responde à pergunta ao usar uma imagem e é necessário ajustar a inserção entre a imagem e o título!
Brody Robertson
23

E para Swift trabalhou isso:

extension UIButton {
    override open var intrinsicContentSize: CGSize {
        let intrinsicContentSize = super.intrinsicContentSize

        let adjustedWidth = intrinsicContentSize.width + titleEdgeInsets.left + titleEdgeInsets.right
        let adjustedHeight = intrinsicContentSize.height + titleEdgeInsets.top + titleEdgeInsets.bottom

        return CGSize(width: adjustedWidth, height: adjustedHeight)
    }
}

Love U Swift

pegpeg
fonte
1
Mesmo que você não deva, é melhor subclassificar neste caso, porque os documentos da Apple afirmam explicitamente que o tamanho intrínseco não inclui titleEdgeInsets em seu cálculo e, ao usar uma extensão, você está violando não apenas as expectativas da Apple, mas todos os outros desenvolvedores que lêem os documentos.
Sirens
18

Esse tópico é um pouco antigo, mas eu mesmo me deparei com isso e fui capaz de resolvê-lo usando uma inserção negativa. Por exemplo, substitua os valores de preenchimento desejados aqui:

UIButton* myButton = [[UIButton alloc] init];
// setup some autolayout constraints here
myButton.titleEdgeInsets = UIEdgeInsetsMake(-desiredBottomPadding,
                                            -desiredRightPadding,
                                            -desiredTopPadding,
                                            -desiredLeftPadding);

Combinado com as restrições corretas de autolayout, você acaba com um botão de redimensionamento automático que contém uma imagem e texto! Visto abaixo com o desiredLeftPaddingconjunto para 10.

Botão com imagem e texto breve

Botão com imagem e texto longo

Você pode ver que o quadro real do botão não abrange o rótulo (já que o rótulo é deslocado 10 pontos para a direita, fora dos limites), mas alcançamos 10 pontos de preenchimento entre o texto e a imagem.

Brian Gerstle
fonte
1
Esta é a solução que eu usei, pois não requer subclassificação. Não vai funcionar se o botão tem um fundo, mas isso não é geralmente um problema com iOS 7
José Manuel Sánchez
Isso funcionará com uma imagem de plano de fundo se você também definir o deslocamento do conteúdo do botão (valor positivo> = inserção do título).
Ben Flynn
9

Para o Swift 3, com base na resposta do pegpeg :

extension UIButton {

    override open var intrinsicContentSize: CGSize {

        let intrinsicContentSize = super.intrinsicContentSize

        let adjustedWidth = intrinsicContentSize.width + titleEdgeInsets.left + titleEdgeInsets.right
        let adjustedHeight = intrinsicContentSize.height + titleEdgeInsets.top + titleEdgeInsets.bottom

        return CGSize(width: adjustedWidth, height: adjustedHeight)

    }

}
TheoK
fonte
Olá. Eu quero usar o botão de extensão personalizada no interfacebuilder. plz help
kemdo 12/07/2018
6

Tudo acima não funcionou para o iOS 9+ , o que fiz foi:

  • Adicione uma restrição de largura (para uma largura mínima quando o botão não tiver texto. O botão será redimensionado automaticamente se o texto for fornecido)
  • defina a relação como Maior que ou Igual

insira a descrição da imagem aqui

Agora, para adicionar uma borda ao redor do botão, basta usar o método:

button.contentEdgeInsets = UIEdgeInsetsMake(0,20,0,20);
Oritm
fonte
Por que não? Ele é dimensionado automaticamente com o conteúdo, basta definir uma largura mínima (que pode ser menor que o texto a ser exibido)
Oritm
Porque você define uma largura mínima. Toda a idéia de execução automática é realizada sem definir nenhuma largura explícita (mínima).
Joris Mans
Não se trata da largura, você pode definir a largura como 1, se preferir, mas o layout automático precisa saber que a largura pode ser igual ou superior . Eu atualizei minha resposta
Oritm
Você não precisa da restrição de largura, o contentEdgeInset é a chave, o layout automático e a usa para tamanho de conteúdo intrínseco.
Chris Conover
5

Eu queria adicionar um espaço de 5 pontos entre o ícone do UIButton e o rótulo. Foi assim que consegui:

UIButton *infoButton = [UIButton buttonWithType:UIButtonTypeCustom];
// more button config etc
infoButton.contentEdgeInsets = UIEdgeInsetsMake(0, 0, 0, 5);
infoButton.titleEdgeInsets = UIEdgeInsetsMake(0, 5, 0, -5);

A maneira como contentEdgeInsets, titleEdgeInsets e imageEdgeInsets se relacionam requer um pouco de troca de informações. Portanto, se você adicionar algumas inserções à esquerda do título, precisará adicionar uma inserção negativa à direita e fornecer mais espaço (via inserção positiva) no conteúdo à direita.

Ao adicionar uma inserção de conteúdo correta para corresponder à mudança das inserções de título, meu texto não sai dos limites do botão.

orj
fonte
3

A opção também está disponível no construtor de interface. Veja o Inset. Coloquei esquerda e direita em 3. Funciona como um encanto.

Captura de tela do construtor de interface

zeiteisen
fonte
1
Sim, como essa resposta explica, a razão pela qual ela funciona é porque você está ajustando o Edge: Content aqui em vez de Edge: Title ou Edge: Image .
smileyborg
1

A solução que eu uso é adicionar uma restrição de largura no botão. Em algum momento da inicialização, após a definição do texto, atualize a restrição de largura da seguinte maneira:

self.buttonWidthConstraint.constant = self.shareButton.intrinsicContentSize.width + 8;

Onde 8 é a sua inserção.

Bob Spryn
fonte
O que é buttonWidthConstraint?
Alexey Golikov
@AlexeyGolikov Um NSLayoutConstraint - developer.apple.com/library/mac/documentation/AppKit/Reference/…
1in9ui5t
1
Essa não é uma ótima solução, porque se o tamanho do conteúdo intrínseco do botão mudar, você precisará atualizar manualmente constanta restrição para o novo valor ... e saber quando é difícil o tamanho do conteúdo intrínseco do botão sem subclassificar o botão.
smileyborg
Ayup. Eu não uso mais esse método. Surpreso foi digno de um voto para baixo, mas _ (ツ) _ / ¯
Bob Spryn
Uma chamada para setNeedsUpdateConstraintspode ser "manualmente" após a atualização do título ou da imagem do botão. Você pode substituir updateConstraintse recalcular buttonWidthConstrainta constante a partir daí. Esta não é necessariamente a melhor abordagem, mas funciona bem o suficiente para mim. YMMV;)
Olivier
1

chamar sizeToFit () garante que contentEdgeInset seja efetivado

extension UIButton {

   open override func draw(_ rect: CGRect) { 
       self.contentEdgeInsets = UIEdgeInsets(top: 10, left: 40, bottom: 10, right: 40)
       self.sizeToFit()
   }
}
Todos
fonte