UIButton: Aumentando a área de ocorrência da área de ocorrência padrão

188

Eu tenho uma pergunta sobre o UIButton e sua área de ocorrência. Estou usando o botão Info Dark no criador de interface, mas estou descobrindo que a área de ocorrência não é grande o suficiente para os dedos de algumas pessoas.

Existe uma maneira de aumentar a área de acerto de um botão programaticamente ou no Interface Builder sem alterar o tamanho do gráfico do InfoButton?

Kevin Bomberry
fonte
Se nada estiver funcionando, basta adicionar um UIButton sem uma imagem e colocar uma sobreposição do ImageView!
Varun
Essa seria a pergunta mais surpreendentemente simples de todo o site, com dezenas de respostas. Quer dizer, eu pode colar a resposta completa aqui: override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return bounds.insetBy(dx: -20, dy: -20).contains(point) }
Fattie

Respostas:

137

Como estou usando uma imagem de plano de fundo, nenhuma dessas soluções funcionou bem para mim. Aqui está uma solução que faz alguma mágica divertida do objetivo-c e oferece uma queda na solução com o mínimo de código.

Primeiro, adicione uma categoria UIButtonque substitua o teste de acerto e também adicione uma propriedade para expandir o quadro de teste de acerto.

UIButton + Extensions.h

@interface UIButton (Extensions)

@property(nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;

@end

UIButton + Extensões.m

#import "UIButton+Extensions.h"
#import <objc/runtime.h>

@implementation UIButton (Extensions)

@dynamic hitTestEdgeInsets;

static const NSString *KEY_HIT_TEST_EDGE_INSETS = @"HitTestEdgeInsets";

-(void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets {
    NSValue *value = [NSValue value:&hitTestEdgeInsets withObjCType:@encode(UIEdgeInsets)];
    objc_setAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS, value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(UIEdgeInsets)hitTestEdgeInsets {
    NSValue *value = objc_getAssociatedObject(self, &KEY_HIT_TEST_EDGE_INSETS);
    if(value) {
        UIEdgeInsets edgeInsets; [value getValue:&edgeInsets]; return edgeInsets;
    }else {
        return UIEdgeInsetsZero;
    }
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    if(UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero) || !self.enabled || self.hidden) {
        return [super pointInside:point withEvent:event];
    }

    CGRect relativeFrame = self.bounds;
    CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets);

    return CGRectContainsPoint(hitFrame, point);
}

@end

Depois que essa classe é adicionada, tudo o que você precisa fazer é definir as bordas do seu botão. Observe que eu escolhi adicionar as inserções; portanto, se você quiser aumentar a área de acerto, use números negativos.

[button setHitTestEdgeInsets:UIEdgeInsetsMake(-10, -10, -10, -10)];

Nota: Lembre-se de importar a categoria ( #import "UIButton+Extensions.h") nas suas aulas.

correr atrás
fonte
26
Substituir um método em uma categoria é uma má ideia. E se outra categoria também tentar substituir pointInside: withEvent :? E a chamada para [super pointInside: withEvent] não está chamando a versão da chamada do UIButton (se houver), mas a versão do pai do UIButton.
honus
@honus Você está certo e a Apple não recomenda fazer isso. Dito isto, contanto que esse código não esteja em uma biblioteca compartilhada e apenas local para o seu aplicativo, você deve estar bem.
Chase
Oi Chase, existe alguma desvantagem de usar uma subclasse?
Sid '16
3
Você está fazendo muito trabalho desnecessário; pode fazer duas linhas simples de código: [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"arrow.png"] forState:UIControlStateNormal]; E apenas defina a largura e a altura necessárias: `backButton.frame = CGRectMake (5, 28, 45, 30);`
Resty
7
@ Resty, ele está usando a imagem de fundo, o que significa que sua abordagem não funcionará.
Mattsson
77

Basta definir os valores inseridos na borda da imagem no criador de interface.

Samuel Goodwin
fonte
1
Eu pensei que isso não funcionaria para mim porque as imagens de plano de fundo não respondem às inserções, mas mudei para definir a propriedade da imagem e, em seguida, defina uma borda esquerda negativa inserida para o meu título, a largura da imagem e ela estava de volta ao topo da imagem. minha imagem.
21813 Dave
12
Isso também pode ser feito em código. Por exemplo,button.imageEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10);
bengoesboom
A resposta de Dave Ross é ótima. Para aqueles que não conseguem visualizá-lo, a imagem-trava seu título para a direita (aparentemente), para que você possa clicar no pequeno clicker no IB para movê-lo de volta (ou no código, como indicado). O único aspecto negativo que vejo é que não consigo usar imagens extensíveis. Pequeno preço a pagar IMO
Paul Bruneau
7
como fazer isso sem esticar a imagem?
zonabi
Defina o modo de conteúdo para algo como Center e não para nada Scaling.
Samuel Goodwin
63

Aqui está uma solução elegante usando Extensões no Swift. Dá a todos os UIButtons uma área atingida de pelo menos 44x44 pontos, conforme as Diretrizes de interface humana da Apple ( https://developer.apple.com/ios/human-interface-guidelines/visual-design/layout/ )

Swift 2:

private let minimumHitArea = CGSizeMake(44, 44)

extension UIButton {
    public override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        // if the button is hidden/disabled/transparent it can't be hit
        if self.hidden || !self.userInteractionEnabled || self.alpha < 0.01 { return nil }

        // increase the hit frame to be at least as big as `minimumHitArea`
        let buttonSize = self.bounds.size
        let widthToAdd = max(minimumHitArea.width - buttonSize.width, 0)
        let heightToAdd = max(minimumHitArea.height - buttonSize.height, 0)
        let largerFrame = CGRectInset(self.bounds, -widthToAdd / 2, -heightToAdd / 2)

        // perform hit test on larger frame
        return (CGRectContainsPoint(largerFrame, point)) ? self : nil
    }
}

Swift 3:

fileprivate let minimumHitArea = CGSize(width: 100, height: 100)

extension UIButton {
    open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // if the button is hidden/disabled/transparent it can't be hit
        if self.isHidden || !self.isUserInteractionEnabled || self.alpha < 0.01 { return nil }

        // increase the hit frame to be at least as big as `minimumHitArea`
        let buttonSize = self.bounds.size
        let widthToAdd = max(minimumHitArea.width - buttonSize.width, 0)
        let heightToAdd = max(minimumHitArea.height - buttonSize.height, 0)
        let largerFrame = self.bounds.insetBy(dx: -widthToAdd / 2, dy: -heightToAdd / 2)

        // perform hit test on larger frame
        return (largerFrame.contains(point)) ? self : nil
    }
}
giaset
fonte
3
Você precisa testar se o botão está oculto no momento. Se sim, retorne nulo.
Josh Gafni 10/09/2015
Obrigado, isso parece ótimo. Quaisquer desvantagens em potencial para estar ciente?
Crashalot #
Eu gosto dessa solução, não exige que eu defina propriedades adicionais em todos os botões. Graças
xtrinch
1
Se as diretrizes da Apple recomendam essas dimensões para a área atingida, por que precisamos corrigir a falha na implementação? Isso é tratado nos SDKs posteriores?
NoodleOfDeath
2
Graças a Deus, temos o Stack Overflow e seus colaboradores. Porque certamente não podemos confiar na Apple para obter informações úteis, oportunas e precisas sobre como corrigir seus problemas básicos mais comuns.
Womble
49

Você também pode UIButtoncriar uma subclasse ou um personalizado UIViewe substituir point(inside:with:)por algo como:

Swift 3

override func point(inside point: CGPoint, with _: UIEvent?) -> Bool {
    let margin: CGFloat = 5
    let area = self.bounds.insetBy(dx: -margin, dy: -margin)
    return area.contains(point)
}

Objetivo-C

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGFloat margin = 5.0;
    CGRect area = CGRectInset(self.bounds, -margin, -margin);
    return CGRectContainsPoint(area, point);
}
jlajlar
fonte
1
obrigado solução muito legal sem categorias adicionais. Além disso, eu simplesmente integro-o aos meus botões que tenho no XIB e funciona bem. resposta ótima
Matrosov Alexander
Esta é uma ótima solução que pode ser estendida a todos os outros controles! Achei útil subclassificar um UISearchBar, por exemplo. O método pointInside está em todos os UIView desde o iOS 2.0. BTW - Swift: func pointInside (_ point: CGPoint, withEvent event: UIEvent!) -> Bool.
Tommie C.
Obrigado por isso! Como já foi dito, isso pode ser realmente útil com outros controles!
Yanchi 17/02
Isso é ótimo!! Obrigado, você me salvou muito tempo! :-)
Vojta 28/10
35

Aqui estão as extensões UIButton + do Chase no Swift 3.0.


import UIKit

private var pTouchAreaEdgeInsets: UIEdgeInsets = .zero

extension UIButton {

    var touchAreaEdgeInsets: UIEdgeInsets {
        get {
            if let value = objc_getAssociatedObject(self, &pTouchAreaEdgeInsets) as? NSValue {
                var edgeInsets: UIEdgeInsets = .zero
                value.getValue(&edgeInsets)
                return edgeInsets
            }
            else {
                return .zero
            }
        }
        set(newValue) {
            var newValueCopy = newValue
            let objCType = NSValue(uiEdgeInsets: .zero).objCType
            let value = NSValue(&newValueCopy, withObjCType: objCType)
            objc_setAssociatedObject(self, &pTouchAreaEdgeInsets, value, .OBJC_ASSOCIATION_RETAIN)
        }
    }

    open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        if UIEdgeInsetsEqualToEdgeInsets(self.touchAreaEdgeInsets, .zero) || !self.isEnabled || self.isHidden {
            return super.point(inside: point, with: event)
        }

        let relativeFrame = self.bounds
        let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.touchAreaEdgeInsets)

        return hitFrame.contains(point)
    }
}

Para usá-lo, você pode:

button.touchAreaEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
dcRay
fonte
1
Na verdade, devemos estender UIControl, não UIButton.
Valentin Shergin
2
E variável pTouchAreaEdgeInsetsdeve ser Int, não UIEdgeInsets. (Na verdade, pode ser de qualquer tipo, mas Inté o tipo mais genérico e simples, por isso é comum nessa construção). (Por isso eu amo essa abordagem em geral Obrigado, dcRay.!)
Valentin Shergin
1
Eu tentei titleEdgeInsets, imageEdgeInsets, contentEdgeInset todos não estão funcionando para mim esta extensão está funcionando bem. Obrigado por postar isso !! Bom trabalho !!
Yogesh Patel
19

Eu recomendo colocar um UIButton com o tipo Custom centrado sobre o botão de informações. Redimensione o botão personalizado para o tamanho desejado para a área de ocorrência. A partir daí, você tem duas opções:

  1. Marque a opção "Mostrar toque no destaque" do botão personalizado. O brilho branco aparecerá sobre o botão de informações, mas na maioria dos casos o dedo do usuário cobrirá isso e tudo o que eles verão é o brilho do lado de fora.

  2. Configure um IBOutlet para o botão de informações e duas IBActions para o botão personalizado, um para 'Touch Down' e outro para o 'Touch Up Inside'. Em seguida, no Xcode, faça com que o evento de touchdown defina a propriedade destacada do botão de informações como YES e o evento touchupinside defina a propriedade destacada como NO.

Kyle
fonte
19

Não defina a backgroundImagepropriedade com sua imagem, defina a imageViewpropriedade. Além disso, verifique se você imageView.contentModedefiniu em UIViewContentModeCenter.

Maurizio
fonte
1
Mais especificamente, defina a propriedade contentMode do botão como UIViewContentModeCenter. Depois, você pode aumentar o quadro do botão que a imagem. Funciona para mim!
Arlomedia
FYI, UIButton não respeita UIViewContentMode: stackoverflow.com/a/4503920/361247
Enrico Susatyo
@EnricoSusatyo desculpe, eu esclarei quais APIs eu estava falando. O imageView.contentModepode ser definido como UIContentModeCenter.
Maurizio
Isso funciona perfeitamente, obrigado por conselhos! [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"image.png"] forState:UIControlStateNormal];
Resty 27/03
@Resty O comentário acima mencionado não funciona para mim: / A imagem está sendo redimensionada para o tamanho de quadro maior.
Anil
14

Minha solução no Swift 3:

class MyButton: UIButton {

    override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let relativeFrame = self.bounds
        let hitTestEdgeInsets = UIEdgeInsetsMake(-25, -25, -25, -25)
        let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets)
        return hitFrame.contains(point)
    }
}
Zhanserik
fonte
12

Não há nada errado com as respostas apresentadas; no entanto, eu queria estender a resposta de jlarjlar, pois possui um potencial incrível que pode agregar valor ao mesmo problema com outros controles (por exemplo, SearchBar). Isso ocorre porque, como pointInside está anexado a um UIView, é possível subclassificar qualquer controle para melhorar a área de toque. Esta resposta também mostra uma amostra completa de como implementar a solução completa.

Crie uma nova subclasse para o seu botão (ou qualquer controle)

#import <UIKit/UIKit.h>

@interface MNGButton : UIButton

@end

Em seguida, substitua o método pointInside em sua implementação de subclasse

@implementation MNGButton


-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    //increase touch area for control in all directions by 20
    CGFloat margin = 20.0;
    CGRect area = CGRectInset(self.bounds, -margin, -margin);
    return CGRectContainsPoint(area, point);
}


@end

No seu arquivo storyboard / xib, selecione o controle em questão e abra o inspetor de identidade e digite o nome da sua classe personalizada.

mngbutton

Na sua classe UIViewController para a cena que contém o botão, altere o tipo de classe do botão para o nome da sua subclasse.

@property (weak, nonatomic) IBOutlet MNGButton *helpButton;

Vincule seu botão storyboard / xib à propriedade IBOutlet e sua área de toque será expandida para caber na área definida na subclasse.

Além de substituir o método pointInside em conjunto com os CGRectInset e CGRectContainsPoint métodos, um deve ter tempo para examinar o CGGeometry para alargar a área de toque rectangular de qualquer subclasse UIView. Você também pode encontrar algumas dicas interessantes sobre casos de uso da CGGeometry no NSHipster .

Por exemplo, pode-se tornar a área de toque irregular usando os métodos mencionados acima ou simplesmente optar por aumentar a área de toque duas vezes maior que a área de toque horizontal:

CGRect area = CGRectInset(self.bounds, -(2*margin), -margin);

NB: Substituir qualquer controle de classe de interface do usuário deve produzir resultados semelhantes ao estender a área de toque para diferentes controles (ou qualquer subclasse UIView, como UIImageView, etc.).

Tommie C.
fonte
10

Isso funciona para mim:

UIButton *button = [UIButton buttonWithType: UIButtonTypeCustom];
// set the image (here with a size of 32 x 32)
[button setImage: [UIImage imageNamed: @"myimage.png"] forState: UIControlStateNormal];
// just set the frame of the button (here 64 x 64)
[button setFrame: CGRectMake(xPositionOfMyButton, yPositionOfMyButton, 64, 64)];
Olof
fonte
3
Isso funciona, mas tenha cuidado se precisar definir uma imagem e um título para um botão. Nesse caso, você precisará usar setBackgoundImage, que dimensionará sua imagem para o novo quadro do botão.
Mihai Damian
7

Não mude o comportamento do UIButton.

@interface ExtendedHitButton: UIButton

+ (instancetype) extendedHitButton;

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

@end

@implementation ExtendedHitButton

+ (instancetype) extendedHitButton {
    return (ExtendedHitButton *) [ExtendedHitButton buttonWithType:UIButtonTypeCustom];
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGRect relativeFrame = self.bounds;
    UIEdgeInsets hitTestEdgeInsets = UIEdgeInsetsMake(-44, -44, -44, -44);
    CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets);
    return CGRectContainsPoint(hitFrame, point);
}

@end
user3109079
fonte
Se o botão for 40x40, esse método não será chamado se os números negativos forem maiores - como alguém mencionado. Caso contrário, isso funciona.
James O'Brien
Isso não funciona para mim. O hitFrame parece calculado corretamente, mas pointInside: nunca é chamado quando o ponto está fora dos limites self.bounds. if (CGRectContainsPoint (hitFrame, point) &&! CGRectContainsPoint (self.bounds, point)) {NSLog (@ "Sucesso!"); // Nunca ligou}
arsenius 28/10
7

Estou usando uma abordagem mais genérica por swizzling -[UIView pointInside:withEvent:]. Isso me permite modificar o comportamento do teste de ocorrência em qualquer um UIView, não apenasUIButton .

Muitas vezes, um botão é colocado dentro de uma exibição de contêiner que também limita o teste de acerto. Por exemplo, quando um botão está na parte superior de uma exibição de contêiner e você deseja estender o alvo de toque para cima, também é necessário estender o alvo de toque da exibição de contêiner.

@interface UIView(Additions)
@property(nonatomic) UIEdgeInsets hitTestEdgeInsets;
@end

@implementation UIView(Additions)

+ (void)load {
    Swizzle(self, @selector(pointInside:withEvent:), @selector(myPointInside:withEvent:));
}

- (BOOL)myPointInside:(CGPoint)point withEvent:(UIEvent *)event {

    if(UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero) || self.hidden ||
       ([self isKindOfClass:UIControl.class] && !((UIControl*)self).enabled))
    {
        return [self myPointInside:point withEvent:event]; // original implementation
    }
    CGRect hitFrame = UIEdgeInsetsInsetRect(self.bounds, self.hitTestEdgeInsets);
    hitFrame.size.width = MAX(hitFrame.size.width, 0); // don't allow negative sizes
    hitFrame.size.height = MAX(hitFrame.size.height, 0);
    return CGRectContainsPoint(hitFrame, point);
}

static char hitTestEdgeInsetsKey;
- (void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets {
    objc_setAssociatedObject(self, &hitTestEdgeInsetsKey, [NSValue valueWithUIEdgeInsets:hitTestEdgeInsets], OBJC_ASSOCIATION_RETAIN);
}

- (UIEdgeInsets)hitTestEdgeInsets {
    return [objc_getAssociatedObject(self, &hitTestEdgeInsetsKey) UIEdgeInsetsValue];
}

void Swizzle(Class c, SEL orig, SEL new) {

    Method origMethod = class_getInstanceMethod(c, orig);
    Method newMethod = class_getInstanceMethod(c, new);

    if(class_addMethod(c, orig, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)))
        class_replaceMethod(c, new, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    else
        method_exchangeImplementations(origMethod, newMethod);
}
@end

O bom dessa abordagem é que você pode usá-la mesmo no Storyboards adicionando um Atributo de Tempo de Execução Definido pelo Usuário. Infelizmente, UIEdgeInsetsnão está diretamente disponível como um tipo lá, mas uma vez que CGRecttambém é composto por uma estrutura com quatro CGFloatele funciona perfeitamente, escolhendo "Rect" e preenchendo os valores como este: {{top, left}, {bottom, right}}.

Ortwin Gentz
fonte
6

Estou usando a seguinte classe no Swift, para também habilitar uma propriedade do Interface Builder para ajustar a margem:

@IBDesignable
class ALExtendedButton: UIButton {

    @IBInspectable var touchMargin:CGFloat = 20.0

    override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
        var extendedArea = CGRectInset(self.bounds, -touchMargin, -touchMargin)
        return CGRectContainsPoint(extendedArea, point)
    }
}
Antoine
fonte
como alterar manualmente este pointInside? Se eu criei um UIButton com margem antiga, mas agora quero alterá-lo?
TomSawyer
5

Bem, você pode colocar seu UIButton dentro de um UIView transparente e um pouco maior e, em seguida, capturar os eventos de toque na instância UIView como no UIButton. Dessa forma, você ainda terá seu botão, mas com uma área de toque maior. Você terá que lidar manualmente com os estados selecionados e destacados com o botão se o usuário tocar na visualização em vez do botão.

Outra possibilidade envolve o uso de um UIImage em vez de um UIButton.

Pablo Santa Cruz
fonte
5

Consegui aumentar a área de ocorrência do botão de informações programaticamente. O gráfico "i" não altera a escala e permanece centralizado no novo quadro de botão.

O tamanho do botão de informações parece estar fixado em 18x19 [*] no Interface Builder. Ao conectá-lo a um IBOutlet, consegui alterar o tamanho do quadro no código sem problemas.

static void _resizeButton( UIButton *button )
{
    const CGRect oldFrame = infoButton.frame;
    const CGFloat desiredWidth = 44.f;
    const CGFloat margin = 
        ( desiredWidth - CGRectGetWidth( oldFrame ) ) / 2.f;
    infoButton.frame = CGRectInset( oldFrame, -margin, -margin );
}

[*]: As versões posteriores do iOS parecem ter aumentado a área de ocorrência do botão de informações.

otto
fonte
5

Esta é a minha solução Swift 3 (com base neste post do blog: http://bdunagan.com/2010/03/01/iphone-tip-larger-hit-area-for-uibutton/ )

class ExtendedHitAreaButton: UIButton {

    @IBInspectable var hitAreaExtensionSize: CGSize = CGSize(width: -10, height: -10)

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

        let extendedFrame: CGRect = bounds.insetBy(dx: hitAreaExtensionSize.width, dy: hitAreaExtensionSize.height)

        return extendedFrame.contains(point) ? self : nil
    }
}
arthurschiller
fonte
3

O teste de acerto personalizado do Chase implementado como uma subclasse de UIButton. Escrito em Objective-C.

Parece funcionar tanto para initebuttonWithType: construtores. Para minhas necessidades, é perfeito, mas desde a subclassificaçãoUIButton pode ser complicada, eu estaria interessado em saber se alguém tem alguma falha nisso.

CustomeHitAreaButton.h

#import <UIKit/UIKit.h>

@interface CustomHitAreaButton : UIButton

- (void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets;

@end

CustomHitAreaButton.m

#import "CustomHitAreaButton.h"

@interface CustomHitAreaButton()

@property (nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;

@end

@implementation CustomHitAreaButton

- (instancetype)initWithFrame:(CGRect)frame {
    if(self = [super initWithFrame:frame]) {
        self.hitTestEdgeInsets = UIEdgeInsetsZero;
    }
    return self;
}

-(void)setHitTestEdgeInsets:(UIEdgeInsets)hitTestEdgeInsets {
    self->_hitTestEdgeInsets = hitTestEdgeInsets;
}

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    if(UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero) || !self.enabled || self.hidden) {
        return [super pointInside:point withEvent:event];
    }
    CGRect relativeFrame = self.bounds;
    CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets);
    return CGRectContainsPoint(hitFrame, point);
}

@end
Phillip Martin
fonte
3

Implementação através da substituição de UIButton herdado.

Swift 2.2 :

// don't forget that negative values are for outset
_button.hitOffset = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
...
class UICustomButton: UIButton {
    var hitOffset = UIEdgeInsets()

    override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
        guard hitOffset != UIEdgeInsetsZero && enabled && !hidden else {
            return super.pointInside(point, withEvent: event)
        }
        return UIEdgeInsetsInsetRect(bounds, hitOffset).contains(point)
    }
}
dimpiax
fonte
2

WJBackgroundInsetButton.h

#import <UIKit/UIKit.h>

@interface WJBackgroundInsetButton : UIButton {
    UIEdgeInsets backgroundEdgeInsets_;
}

@property (nonatomic) UIEdgeInsets backgroundEdgeInsets;

@end

WJBackgroundInsetButton.m

#import "WJBackgroundInsetButton.h"

@implementation WJBackgroundInsetButton

@synthesize backgroundEdgeInsets = backgroundEdgeInsets_;

-(CGRect) backgroundRectForBounds:(CGRect)bounds {
    CGRect sup = [super backgroundRectForBounds:bounds];
    UIEdgeInsets insets = self.backgroundEdgeInsets;
    CGRect r = UIEdgeInsetsInsetRect(sup, insets);
    return r;
}

@end
William Jockusch
fonte
2

Nunca substitua o método na categoria. Subclasse e substitua - pointInside:withEvent:. Por exemplo, se o lado do seu botão for menor que 44 px (recomendado como área de toque mínimo), use o seguinte:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    return (ABS(point.x - CGRectGetMidX(self.bounds)) <= MAX(CGRectGetMidX(self.bounds), 22)) && (ABS(point.y - CGRectGetMidY(self.bounds)) <= MAX(CGRectGetMidY(self.bounds), 22));
}
user500
fonte
2

Eu fiz uma biblioteca para esse propósito.

Você pode optar por usar uma UIViewcategoria, sem necessidade de subclassificação :

@interface UIView (KGHitTesting)
- (void)setMinimumHitTestWidth:(CGFloat)width height:(CGFloat)height;
@end

Ou você pode subclassificar seu UIView ou UIButton e definir o minimumHitTestWidthe / ouminimumHitTestHeight . A área de teste de batida do botão será representada por esses 2 valores.

Assim como outras soluções, ele usa o - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)eventmétodo O método é chamado quando o iOS executa o teste de acerto. este postagem do blog tem uma boa descrição de como o teste de impacto do iOS funciona.

https://github.com/kgaidis/KGHitTestingViews

@interface KGHitTestingButton : UIButton <KGHitTesting>

@property (nonatomic) CGFloat minimumHitTestHeight; 
@property (nonatomic) CGFloat minimumHitTestWidth;

@end

Você também pode apenas subclasse e usar o Interface Builder sem escrever nenhum código: insira a descrição da imagem aqui

kgaidis
fonte
1
Acabei instalando isso apenas por uma questão de velocidade. Adicionar o IBInspectable foi uma ótima idéia, obrigado por montar isso.
precisa saber é o seguinte
2

Baseado na resposta do giaset acima (que achei a solução mais elegante), aqui está a versão rápida 3:

import UIKit

fileprivate let minimumHitArea = CGSize(width: 44, height: 44)

extension UIButton {
    open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // if the button is hidden/disabled/transparent it can't be hit
        if isHidden || !isUserInteractionEnabled || alpha < 0.01 { return nil }

        // increase the hit frame to be at least as big as `minimumHitArea`
        let buttonSize = bounds.size
        let widthToAdd = max(minimumHitArea.width - buttonSize.width, 0)
        let heightToAdd = max(minimumHitArea.height - buttonSize.height, 0)
        let largerFrame = bounds.insetBy(dx: -widthToAdd / 2, dy: -heightToAdd / 2)

        // perform hit test on larger frame
        return (largerFrame.contains(point)) ? self : nil
    }
}
Nhon Nguyen
fonte
2

É simples assim:

class ChubbyButton: UIButton {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        return bounds.insetBy(dx: -20, dy: -20).contains(point)
    }
}

Essa é a coisa toda.

Fattie
fonte
1

Estou usando este truque para o botão dentro do tableviewcell.accessoryView para aumentar sua área de toque

#pragma mark - Touches

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch                  = [touches anyObject];
    CGPoint location                = [touch locationInView:self];
    CGRect  accessoryViewTouchRect  = CGRectInset(self.accessoryView.frame, -15, -15);

    if(!CGRectContainsPoint(accessoryViewTouchRect, location))
        [super touchesBegan:touches withEvent:event];
}

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
{
    UITouch *touch                  = [touches anyObject];
    CGPoint location                = [touch locationInView:self];
    CGRect  accessoryViewTouchRect  = CGRectInset(self.accessoryView.frame, -15, -15);

    if(CGRectContainsPoint(accessoryViewTouchRect, location) && [self.accessoryView isKindOfClass:[UIButton class]])
    {
        [(UIButton *)self.accessoryView sendActionsForControlEvents:UIControlEventTouchUpInside];
    }
    else
        [super touchesEnded:touches withEvent:event];
}
abuharsky
fonte
1

Acabei de fazer a porta da solução @Chase no Swift 2.2

import Foundation
import ObjectiveC

private var hitTestEdgeInsetsKey: UIEdgeInsets = UIEdgeInsetsZero

extension UIButton {
    var hitTestEdgeInsets:UIEdgeInsets {
        get {
            let inset = objc_getAssociatedObject(self, &hitTestEdgeInsetsKey) as? NSValue ?? NSValue(UIEdgeInsets: UIEdgeInsetsZero)
            return inset.UIEdgeInsetsValue()
        }
        set {
            let inset = NSValue(UIEdgeInsets: newValue)
            objc_setAssociatedObject(self, &hitTestEdgeInsetsKey, inset, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC)
        }
    }

    public override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
        guard !UIEdgeInsetsEqualToEdgeInsets(hitTestEdgeInsets, UIEdgeInsetsZero) && self.enabled == true && self.hidden == false else {
            return super.pointInside(point, withEvent: event)
        }
        let relativeFrame = self.bounds
        let hitFrame = UIEdgeInsetsInsetRect(relativeFrame, hitTestEdgeInsets)
        return CGRectContainsPoint(hitFrame, point)
    }
}

e você pode usar assim

button.hitTestEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10)

Para qualquer outra referência, consulte https://stackoverflow.com/a/13067285/1728552

Serluca
fonte
1

Resposta de @ antoine formatada para Swift 4

class ExtendedHitButton: UIButton
{
    override func point( inside point: CGPoint, with event: UIEvent? ) -> Bool
    {
        let relativeFrame = self.bounds
        let hitTestEdgeInsets = UIEdgeInsetsMake( -44, -44, -44, -44 ) // Apple recommended hit target
        let hitFrame = UIEdgeInsetsInsetRect( relativeFrame, hitTestEdgeInsets )
        return hitFrame.contains( point );
    }
}
spfursich
fonte
0

Eu segui a resposta do Chase e funciona muito bem, um único problema quando você cria a área muito grande, maior que a zona em que o botão é desmarcado (se a zona não for maior), ele não chama o seletor para o evento UIControlEventTouchUpInside .

Eu acho que o tamanho é superior a 200 em qualquer direção ou algo assim.

Dragão irreal
fonte
0

Estou muito atrasado para este jogo, mas queria analisar uma técnica simples que pode resolver seus problemas. Aqui está um trecho UIButton programático típico para mim:

UIImage *arrowImage = [UIImage imageNamed:@"leftarrow"];
arrowButton = [[UIButton alloc] initWithFrame:CGRectMake(15.0, self.frame.size.height-35.0, arrowImage.size.width/2, arrowImage.size.height/2)];
[arrowButton setBackgroundImage:arrowImage forState:UIControlStateNormal];
[arrowButton addTarget:self action:@selector(onTouchUp:) forControlEvents:UIControlEventTouchUpOutside];
[arrowButton addTarget:self action:@selector(onTouchDown:) forControlEvents:UIControlEventTouchDown];
[arrowButton addTarget:self action:@selector(onTap:) forControlEvents:UIControlEventTouchUpInside];
[arrowButton addTarget:self action:@selector(onTouchUp:) forControlEvents:UIControlEventTouchDragExit];
[arrowButton setUserInteractionEnabled:TRUE];
[arrowButton setAdjustsImageWhenHighlighted:NO];
[arrowButton setTag:1];
[self addSubview:arrowButton];

Estou carregando uma imagem png transparente para o meu botão e definindo a imagem de fundo. Estou definindo o quadro com base na UIImage e redimensionando em 50% a retina. OK, talvez você concorde com o exposto acima ou não, mas se você quiser aumentar a área atingida e economizar uma dor de cabeça:

O que faço, abra a imagem no photoshop e simplesmente aumente o tamanho da tela para 120% e economize. Efetivamente, você acabou de aumentar a imagem com pixels transparentes.

Apenas uma abordagem.

chrisallick
fonte
0

A resposta de @jlajlar acima parecia boa e direta, mas não corresponde ao Xamarin.iOS, então eu o convertei no Xamarin. Se você está procurando uma solução em um Xamarin iOS, aqui está:

public override bool PointInside (CoreGraphics.CGPoint point, UIEvent uievent)
{
    var margin = -10f;
    var area = this.Bounds;
    var expandedArea = area.Inset(margin, margin);
    return expandedArea.Contains(point);
}

Você pode adicionar esse método à classe em que você está substituindo o UIView ou o UIImageView. Isso funcionou muito bem :)

Possui AlTaiar
fonte
0

Nenhuma das respostas funciona perfeita para mim, porque eu uso a imagem de plano de fundo e um título nesse botão. Além disso, o botão será redimensionado conforme o tamanho da tela for alterado.

Em vez disso, amplio a área da torneira aumentando a área transparente do png.

Jakehao
fonte