UIButton: hacer que el área de impacto sea más grande que el área de impacto predeterminada

188

Tengo una pregunta sobre UIButton y su área de impacto. Estoy usando el botón Info Dark en el generador de interfaces, pero descubro que el área de impacto no es lo suficientemente grande para los dedos de algunas personas.

¿Hay alguna forma de aumentar el área de pulsación de un botón ya sea mediante programación o en Interface Builder sin cambiar el tamaño del gráfico InfoButton?

Kevin Bomberry
fuente
Si nada funciona, simplemente agregue un UIButton sin una imagen y luego coloque una superposición ImageView.
Varun
Esta tendría que ser la pregunta más asombrosamente simple en todo el sitio, que tiene docenas de respuestas. Quiero decir, puedo pegar la respuesta completa aquí: override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return bounds.insetBy(dx: -20, dy: -20).contains(point) }
Fattie

Respuestas:

137

Como estoy usando una imagen de fondo, ninguna de estas soluciones funcionó bien para mí. Aquí hay una solución que hace un poco de magia divertida del objetivo-c y ofrece una solución de caída con un código mínimo.

Primero, agregue una categoría UIButtonque anule la prueba de éxito y también agregue una propiedad para expandir el marco de prueba de éxito.

UIButton + Extensiones.h

@interface UIButton (Extensions)

@property(nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;

@end

UIButton + Extensiones.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

Una vez que se agrega esta clase, todo lo que necesita hacer es configurar las inserciones de borde de su botón. Tenga en cuenta que elegí agregar las inserciones, por lo que si desea agrandar el área de impacto, debe usar números negativos.

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

Nota: Recuerde importar la categoría ( #import "UIButton+Extensions.h") en sus clases.

Persecución
fuente
26
Anular un método en una categoría es una mala idea. ¿Qué sucede si otra categoría también intenta anular pointInside: withEvent :? Y la llamada a [super pointInside: withEvent] no está llamando a la versión de UIButton de la llamada (si tiene una) sino a la versión principal de UIButton.
honus
@honus Tienes razón y Apple no recomienda hacer esto. Dicho esto, siempre que este código no esté en una biblioteca compartida y solo sea local para su aplicación, debería estar bien.
Chase
Hola Chase, ¿hay alguna desventaja de usar una subclase en su lugar?
Sid
3
Está haciendo demasiado trabajo innecesario, puede hacer dos líneas simples de código: [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"arrow.png"] forState:UIControlStateNormal];y simplemente establecer el ancho y la altura que necesita: `backButton.frame = CGRectMake (5, 28, 45, 30);`
Resty
77
@Resty, está usando la imagen de fondo, lo que significa que su enfoque no funcionará.
mattsson 01 de
77

Simplemente configure los valores de inserción del borde de la imagen en el generador de interfaces.

Samuel Goodwin
fuente
1
Pensé que esto no funcionaría para mí porque las imágenes de fondo no responden a las inserciones, pero cambié la configuración de la propiedad de la imagen y luego configuré un recuadro negativo para mi título, el ancho de la imagen y estaba de nuevo encima de Mi imagen.
Dave Ross
12
Esto también se puede hacer en código. Por ejemplo,button.imageEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10);
bengoesboom
La respuesta de Dave Ross es genial. Para aquellos que no pueden imaginarlo, la imagen cambia su título a la derecha de sí mismo (aparentemente), por lo que puede hacer clic en el pequeño clicker en IB para moverlo hacia atrás (o en el código como se indicó). Lo único negativo que veo es que no puedo usar imágenes estirables. Pequeño precio a pagar en la OMI
Paul Bruneau
77
¿Cómo hacer esto sin estirar la imagen?
zonabi
Establezca el modo de contenido en algo como Centro y no en nada Escala.
Samuel Goodwin el
63

Aquí hay una solución elegante usando Extensiones en Swift. Le da a todos los UIButtons un área de impacto de al menos 44x44 puntos, según las Pautas de interfaz humana de 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
fuente
3
Debe probar si el botón está actualmente oculto. Si es así, regrese nulo.
Josh Gafni
Gracias, esto se ve genial. ¿Alguna desventaja potencial a tener en cuenta?
Crashalot
Me gusta esta solución, no requiere que establezca propiedades adicionales en todos los botones. Gracias
xtrinch
1
Si las pautas de Apple recomiendan estas dimensiones para el área afectada, ¿por qué obligarnos a corregir su falla de implementación? ¿Se aborda esto en los SDK posteriores?
NoodleOfDeath
2
Gracias a Dios tenemos Stack Overflow y sus colaboradores. Porque ciertamente no podemos confiar en Apple para obtener información útil, oportuna y precisa sobre cómo solucionar sus problemas más comunes y básicos.
Womble
49

También podría subclasificar UIButtono personalizar UIViewy anular point(inside:with:)con 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)
}

C objetivo

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGFloat margin = 5.0;
    CGRect area = CGRectInset(self.bounds, -margin, -margin);
    return CGRectContainsPoint(area, point);
}
jlajlar
fuente
1
gracias solución realmente genial sin ninguna categoría adicional. También lo integro con mis botones que tengo en XIB y funciona bien. gran respuesta
Matrosov Alexander
¡Esta es una gran solución que se puede extender a todos los demás controles! Me pareció útil subclasificar una UISearchBar, por ejemplo. El método pointInside está en cada UIView desde iOS 2.0. Por cierto - Swift: func pointInside (_ point: CGPoint, withEvent event: UIEvent!) -> Bool.
Tommie C.
¡Gracias por esto! Como dijo otro, ¡esto puede ser realmente útil con otros controles!
Yanchi
¡¡Esto es genial!! ¡Gracias, me has ahorrado mucho tiempo! :-)
Vojta
35

Aquí están las extensiones UIButton + de Chase en 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 usarlo, puedes:

button.touchAreaEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
dcRay
fuente
1
En realidad, deberíamos extender UIControl, no UIButton.
Valentin Shergin
2
Y la variable pTouchAreaEdgeInsetsdebe ser Int, no UIEdgeInsets. (En realidad, puede ser de cualquier tipo, pero Intes el tipo más genérico y simple, por lo que es común en dicha construcción). (Por lo tanto, me encanta este enfoque en general. ¡Gracias, dcRay!)
Valentin Shergin
1
He intentado titleEdgeInsets, imageEdgeInsets, contentEdgeInset, todos no funcionan para mí, esta extensión está funcionando bien. ¡Gracias por publicar esto! Gran trabajo !!
Yogesh Patel
19

Recomiendo colocar un UIButton con el tipo Personalizado centrado sobre su botón de información. Cambie el tamaño del botón personalizado al tamaño que desea que tenga el área de impacto. A partir de ahí tienes dos opciones:

  1. Marque la opción 'Mostrar toque al resaltar' del botón personalizado. El brillo blanco aparecerá sobre el botón de información, pero en la mayoría de los casos el dedo del usuario cubrirá esto y todo lo que verán es el brillo alrededor del exterior.

  2. Configure un IBOutlet para el botón de información y dos IBActions para el botón personalizado, uno para 'Touch Down' y otro para 'Touch Up Inside'. Luego, en Xcode, haga que el evento de touchdown establezca la propiedad resaltada del botón de información en YES y el evento touchupinside establezca la propiedad resaltada en NO.

Kyle
fuente
19

No establezca la backgroundImagepropiedad con su imagen, establezca la imageViewpropiedad. Además, asegúrese de haber imageView.contentModeestablecido en UIViewContentModeCenter.

Maurizio
fuente
1
Más específicamente, establezca la propiedad contentMode del botón en UIViewContentModeCenter. Luego puede hacer que el marco del botón sea más grande que la imagen. ¡Funciona para mi!
arlomedia
Para su información, UIButton no respeta UIViewContentMode: stackoverflow.com/a/4503920/361247
Enrico Susatyo
@EnricoSusatyo lo siento, he aclarado de qué API estaba hablando. El imageView.contentModese puede configurar como UIContentModeCenter.
Maurizio
Esto funciona perfectamente, gracias por el consejo! [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"image.png"] forState:UIControlStateNormal];
Resty
@Resty El comentario mencionado anteriormente no funciona para mí: / La imagen se está redimensionando al tamaño de marco establecido más grande.
Anil
14

Mi solución en 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
fuente
12

No hay nada malo con las respuestas presentadas; sin embargo, quería extender la respuesta de jlarjlar ya que tiene un potencial sorprendente que puede agregar valor al mismo problema con otros controles (por ejemplo, SearchBar). Esto se debe a que ya que pointInside está conectado a una UIView, se puede subclasificar cualquier control para mejorar el área táctil. Esta respuesta también muestra una muestra completa de cómo implementar la solución completa.

Cree una nueva subclase para su botón (o cualquier control)

#import <UIKit/UIKit.h>

@interface MNGButton : UIButton

@end

A continuación, anule el método pointInside en su implementación de subclase

@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

En su archivo de guión gráfico / xib, seleccione el control en cuestión y abra el inspector de identidad y escriba el nombre de su clase personalizada.

botón mng

En su clase UIViewController para la escena que contiene el botón, cambie el tipo de clase para el botón al nombre de su subclase.

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

Enlace su storyboard / botón xib a la propiedad IBOutlet y su área táctil se expandirá para adaptarse al área definida en la subclase.

Además de anular el método pointInside junto con los métodos CGRectInset y CGRectContainsPoint , uno debería tomarse un tiempo para examinar CGGeometry para extender el área táctil rectangular de cualquier subclase UIView. También puede encontrar algunos buenos consejos sobre casos de uso de CGGeometry en NSHipster .

Por ejemplo, uno podría hacer que el área táctil sea irregular utilizando los métodos mencionados anteriormente o simplemente elegir hacer que el área táctil de ancho sea dos veces más grande que el área táctil horizontal:

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

NB: la sustitución de cualquier control de clase UI debería producir resultados similares al extender el área táctil para diferentes controles (o cualquier subclase UIView, como UIImageView, etc.).

Tommie C.
fuente
10

Esto funciona para mi:

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
fuente
3
Esto funciona, pero tenga cuidado si necesita configurar una imagen y un título para un botón. En ese caso, deberá usar setBackgoundImage, que escalará su imagen al nuevo marco del botón.
Mihai Damian
7

No cambie el comportamiento de 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
usuario3109079
fuente
Si su botón es 40x40, este método no se llamará si los números negativos son mayores, como alguien más mencionó. De lo contrario, esto funciona.
James O'Brien
Esto no funciona para mi. hitFrame aparece calculado correctamente, pero pointInside: nunca se llama cuando el punto está fuera de self.bounds. if (CGRectContainsPoint (hitFrame, point) &&! CGRectContainsPoint (self.bounds, point)) {NSLog (@ "Success!"); // Nunca llamé}
arsenius
7

Estoy usando un enfoque más genérico por swizzling -[UIView pointInside:withEvent:]. Esto me permite modificar el comportamiento de las pruebas de impacto en cualquiera UIView, no solo UIButton.

A menudo, se coloca un botón dentro de una vista de contenedor que también limita la prueba de impacto. Por ejemplo, cuando un botón está en la parte superior de una vista de contenedor y desea extender el objetivo táctil hacia arriba, también debe extender el objetivo táctil de la vista de contenedor.

@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

Lo bueno de este enfoque es que puede usar esto incluso en Storyboards agregando un Atributo de tiempo de ejecución definido por el usuario. Por desgracia, UIEdgeInsetsno está disponible directamente como un tipo allí, pero ya CGRecttambién se compone de una estructura con cuatro CGFloatfunciona perfectamente eligiendo "Rect" y rellenar los valores de este tipo: {{top, left}, {bottom, right}}.

Ortwin Gentz
fuente
6

Estoy usando la siguiente clase en Swift, para habilitar también una propiedad de Interface Builder para ajustar el margen:

@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
fuente
¿Cómo cambiar manualmente este punto dentro? Si he creado un UIButton con un margen antiguo, pero ahora quiero cambiarlo.
TomSawyer
5

Bueno, puede colocar su UIButton dentro de una UIView transparente y ligeramente más grande, y luego capturar los eventos táctiles en la instancia de UIView como en la UIButton. De esa manera, aún tendrá su botón, pero con un área táctil más grande. Tendrá que lidiar manualmente con los estados seleccionados y resaltados con el botón si el usuario toca la vista en lugar del botón.

Otra posibilidad implica usar un UIImage en lugar de un UIButton.

Pablo Santa Cruz
fuente
5

He podido aumentar el área de impacto del botón de información mediante programación. El gráfico "i" no cambia de escala y permanece centrado en el nuevo marco del botón.

El tamaño del botón de información parece estar fijado a 18x19 [*] en Interface Builder. Al conectarlo a un IBOutlet, pude cambiar su tamaño de cuadro en el código sin ningún problema.

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 );
}

[*]: Parece que las versiones posteriores de iOS han aumentado el área de impacto del botón de información.

otón
fuente
5

Esta es mi solución Swift 3 (basada en este 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
    }
}
arturschiller
fuente
3

Prueba de éxito personalizada de Chase implementada como una subclase de UIButton. Escrito en Objective-C.

Parece que funciona tanto para inity buttonWithType:constructores. Para mis necesidades es perfecto, pero como la subclasificación UIButtonpuede ser complicada, me interesaría saber si alguien tiene una falla.

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
fuente
3

Implementación mediante anulación de UIButton heredado.

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
fuente
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
fuente
2

Nunca anule el método en la categoría. Botón de subclase y anulación - pointInside:withEvent:. Por ejemplo, si el lado de su botón es más pequeño que 44 px (que se recomienda como área mínima tocable) use esto:

- (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));
}
usuario500
fuente
2

Hice una biblioteca para este mismo propósito.

Puede elegir usar una UIViewcategoría, no se requieren subclases :

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

O puede subclasificar su UIView o UIButton y establecer el minimumHitTestWidthy / o minimumHitTestHeight. El área de prueba de acierto de los botones estará representada por estos 2 valores.

Al igual que otras soluciones, utiliza el - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)eventmétodo. Se llama al método cuando iOS realiza pruebas de impacto. Esta publicación de blog tiene una buena descripción de cómo funciona la prueba de impacto de iOS.

https://github.com/kgaidis/KGHitTestingViews

@interface KGHitTestingButton : UIButton <KGHitTesting>

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

@end

También puede subclasificar y usar el Creador de interfaces sin escribir ningún código: ingrese la descripción de la imagen aquí

kgaidis
fuente
1
Terminé instalando esto solo por el bien de la velocidad. Agregar IBInspectable fue una gran idea gracias por armar esto.
user3344977
2

Base en la respuesta anterior de giaset (que encontré la solución más elegante), aquí está la versión 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
fuente
2

Es así de simple:

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

Eso es todo.

Fattie
fuente
1

Estoy usando este truco para el botón dentro de tableviewcell.accessoryView para agrandar su área táctil

#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
fuente
1

Acabo de hacer el puerto de la solución @Chase en 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)
    }
}

y puedes usar así

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

Para cualquier otra referencia, consulte https://stackoverflow.com/a/13067285/1728552

Serluca
fuente
1

Respuesta de @ antoine formateada 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
fuente
0

He seguido la respuesta de Chase y funciona muy bien, un solo problema cuando creas el área demasiado grande, más grande que la zona donde el botón se deselecciona (si la zona no era más grande) no llama al selector para el evento UIControlEventTouchUpInside .

Creo que el tamaño es superior a 200 en cualquier dirección o algo así.

Dragón irreal
fuente
0

Llegué muy tarde a este juego, pero quería opinar sobre una técnica simple que podría resolver sus problemas. Aquí hay un fragmento programático típico de UIButton para mí:

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];

Estoy cargando una imagen png transparente para mi botón y estableciendo la imagen de fondo. Estoy configurando el marco basado en el UIImage y escalando un 50% para la retina. OK, tal vez estés de acuerdo con lo anterior o no, PERO si quieres hacer que el área de golpe sea MÁS GRANDE y ahorrarte un dolor de cabeza:

Lo que hago es abrir la imagen en Photoshop y simplemente aumentar el tamaño del lienzo al 120% y guardar. Efectivamente, acaba de agrandar la imagen con píxeles transparentes.

Solo un enfoque.

chrisallick
fuente
0

La respuesta de @jlajlar anterior parecía buena y directa, pero no coincide con Xamarin.iOS, así que la convertí en Xamarin. Si está buscando una solución en un iOS de Xamarin, aquí 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);
}

Puede agregar este método a la clase donde está anulando UIView o UIImageView. Esto funcionó muy bien :)

Tiene AlTaiar
fuente
0

Ninguna de las respuestas funciona perfectamente para mí, porque uso la imagen de fondo y un título en ese botón. Además, el botón cambiará de tamaño a medida que cambie el tamaño de la pantalla.

En cambio, agrando el área del grifo haciendo que el área transparente PNG sea más grande.

Jakehao
fuente