UIButton: Rendre la zone de hit plus grande que la zone de hit par défaut

188

J'ai une question concernant UIButton et sa zone touchée. J'utilise le bouton Info Dark dans le générateur d'interface, mais je constate que la zone de frappe n'est pas assez grande pour les doigts de certaines personnes.

Existe-t-il un moyen d'augmenter la zone de sélection d'un bouton par programme ou dans Interface Builder sans modifier la taille du graphique InfoButton?

Kevin Bomberry
la source
Si rien ne fonctionne, ajoutez simplement un UIButton sans image, puis mettez une superposition ImageView!
Varun
Cela devrait être la question la plus étonnamment simple de tout le site, qui contient des dizaines de réponses. Je veux dire, je peux coller la réponse entière ici: override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return bounds.insetBy(dx: -20, dy: -20).contains(point) }
Fattie

Réponses:

137

Puisque j'utilise une image d'arrière-plan, aucune de ces solutions n'a bien fonctionné pour moi. Voici une solution qui fait de la magie objective-c amusante et offre une solution de remplacement avec un code minimal.

Tout d'abord, ajoutez une catégorie à UIButtoncelle qui remplace le test de positionnement et ajoute également une propriété pour développer le cadre de test de positionnement.

UIButton + Extensions.h

@interface UIButton (Extensions)

@property(nonatomic, assign) UIEdgeInsets hitTestEdgeInsets;

@end

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

Une fois cette classe ajoutée, tout ce que vous avez à faire est de définir les encarts de bord de votre bouton. Notez que j'ai choisi d'ajouter les encarts, donc si vous voulez agrandir la zone de frappe, vous devez utiliser des nombres négatifs.

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

Remarque: n'oubliez pas d'importer la catégorie ( #import "UIButton+Extensions.h") dans vos classes.

Chasse
la source
26
Remplacer une méthode dans une catégorie est une mauvaise idée. Que faire si une autre catégorie essaie également de remplacer pointInside: withEvent:? Et l'appel à [super pointInside: withEvent] n'appelle pas la version UIButton de l'appel (s'il en a une) mais la version parent d'UIButton.
honus
@honus Vous avez raison et Apple ne recommande pas de faire cela. Cela étant dit, tant que ce code n'est pas dans une bibliothèque partagée et uniquement local à votre application, tout devrait bien se passer.
Chase le
Salut Chase, y a-t-il un inconvénient à utiliser une sous-classe à la place?
Sid
3
Vous faites trop de travail inutile, vous pouvez faire deux simples lignes de code: [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"arrow.png"] forState:UIControlStateNormal];Et définissez simplement la largeur et la hauteur dont vous avez besoin: `backButton.frame = CGRectMake (5, 28, 45, 30);`
Resty
7
@Resty, il utilise l'image d'arrière-plan, ce qui signifie que votre approche ne fonctionnera pas.
mattsson
77

Définissez simplement les valeurs d'encart de bord de l'image dans le générateur d'interface.

Samuel Goodwin
la source
1
Je pensais que cela ne fonctionnerait pas pour moi parce que les images d'arrière-plan ne répondent pas aux encarts, mais j'ai changé pour définir la propriété d'image, puis j'ai défini un encart de bord gauche négatif pour mon titre, la largeur de l'image et il était de retour au-dessus de mon image.
Dave Ross
12
Cela peut également être fait dans le code. Par exemple,button.imageEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10);
bengoesboom
La réponse de Dave Ross est excellente. Pour ceux qui ne peuvent pas l'imaginer, l'image fait basculer votre titre à droite de lui-même (apparemment), vous pouvez donc cliquer sur le petit clicker dans IB pour le déplacer en arrière (ou dans le code comme indiqué). Le seul point négatif que je vois est que je ne peux pas utiliser d'images extensibles. Petit prix à payer IMO
Paul Bruneau
7
comment faire cela sans étirer l'image?
zonabi
Réglez le mode de contenu sur quelque chose comme Centre et non sur une échelle.
Samuel Goodwin
63

Voici une solution élégante utilisant les extensions dans Swift. Il donne à tous les UIButtons une zone de frappe d'au moins 44x44 points, conformément aux directives d'interface humaine d'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
la source
3
Vous devez tester si le bouton est actuellement masqué. Si tel est le cas, renvoyez nul.
Josh Gafni
Merci, ça a l'air génial. Y a-t-il des inconvénients potentiels à prendre en compte?
Crashalot
J'aime cette solution, ne m'oblige pas à définir des propriétés supplémentaires sur tous les boutons. Merci
xtrinch
1
Si les directives Apple recommandent ces dimensions pour la zone touchée, pourquoi nous obliger à corriger leur échec de mise en œuvre? Est-ce abordé dans les SDK ultérieurs?
NoodleOfDeath
2
Dieu merci, nous avons Stack Overflow et ses contributeurs. Parce que nous ne pouvons certainement pas compter sur Apple pour obtenir des informations utiles, opportunes et précises sur la façon de résoudre leurs problèmes de base les plus courants.
Womble
49

Vous pouvez également sous UIButton- classe ou une coutume UIViewet remplacer point(inside:with:)avec quelque chose comme:

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

Objectif c

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGFloat margin = 5.0;
    CGRect area = CGRectInset(self.bounds, -margin, -margin);
    return CGRectContainsPoint(area, point);
}
jlajlar
la source
1
merci solution vraiment cool sans aucune catégorie supplémentaire. De plus, je l'intègre simplement avec mes boutons que j'ai dans XIB et cela fonctionne bien. bonne réponse
Matrosov Alexander
C'est une excellente solution qui peut être étendue à toutes les autres commandes! J'ai trouvé utile de sous-classer une UISearchBar par exemple. La méthode pointInside est sur chaque UIView depuis iOS 2.0. BTW - Swift: func pointInside (_ point: CGPoint, withEvent event: UIEvent!) -> Bool.
Tommie C.
Merci pour cela! Comme d'autres l'ont dit, cela peut être vraiment utile avec d'autres contrôles!
Yanchi
C'est bien!! Merci, tu m'as fait gagner beaucoup de temps! :-)
Vojta
35

Voici les extensions UIButton + de Chase dans 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)
    }
}

Pour l'utiliser, vous pouvez:

button.touchAreaEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
dcRay
la source
1
En fait, nous devrions étendre UIControl, non UIButton.
Valentin Shergin
2
Et la variable pTouchAreaEdgeInsetsdoit être Int, non UIEdgeInsets. (En fait, il peut s'agir de n'importe quel type, mais Intc'est le type le plus générique et le plus simple, il est donc courant dans une telle construction). (Par conséquent, j'adore cette approche en général. Merci, dcRay!)
Valentin Shergin
1
J'ai essayé titleEdgeInsets, imageEdgeInsets, contentEdgeInset tous ne fonctionnent pas pour moi, cette extension fonctionne bien. Merci d'avoir posté ceci !! Bon travail !!
Yogesh Patel
19

Je recommande de placer un UIButton avec le type Personnalisé centré sur votre bouton d'information. Redimensionnez le bouton personnalisé à la taille souhaitée pour la zone de frappe. De là, vous avez deux options:

  1. Cochez l'option «Afficher le toucher en surbrillance» du bouton personnalisé. La lueur blanche apparaîtra sur le bouton d'information, mais dans la plupart des cas, le doigt de l'utilisateur couvrira cela et tout ce qu'ils verront, c'est la lueur autour de l'extérieur.

  2. Configurez un IBOutlet pour le bouton info et deux IBActions pour le bouton personnalisé, un pour «Touch Down» et un pour «Touch Up Inside». Ensuite, dans Xcode, faites en sorte que l'événement touchdown définisse la propriété en surbrillance du bouton d'information sur YES et que l'événement touchupinside définisse la propriété en surbrillance sur NON.

Kyle
la source
19

Ne définissez pas la backgroundImagepropriété avec votre image, définissez la imageViewpropriété. Assurez-vous également que vous avez imageView.contentModedéfini sur UIViewContentModeCenter.

Maurizio
la source
1
Plus précisément, définissez la propriété contentMode du bouton sur UIViewContentModeCenter. Ensuite, vous pouvez agrandir le cadre du bouton par rapport à l'image. Travaille pour moi!
arlomedia
Pour info, UIButton ne respecte pas UIViewContentMode: stackoverflow.com/a/4503920/361247
Enrico Susatyo
@EnricoSusatyo désolé, j'ai clarifié les API dont je parlais. Le imageView.contentModepeut être défini comme UIContentModeCenter.
Maurizio
Cela fonctionne parfaitement, merci pour les conseils! [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"image.png"] forState:UIControlStateNormal];
Resty
@Resty Le commentaire mentionné ci-dessus ne fonctionne pas pour moi: / L'image est redimensionnée à la plus grande taille de cadre définie.
Anil
14

Ma solution sur 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
la source
12

Il n'y a rien de mal avec les réponses présentées; Cependant, je voulais étendre la réponse de jlarjlar car elle possède un potentiel incroyable qui peut ajouter de la valeur au même problème avec d'autres contrôles (par exemple, SearchBar). En effet, puisque pointInside est attaché à un UIView, il est possible de sous-classer n'importe quel contrôle pour améliorer la zone tactile. Cette réponse montre également un exemple complet de mise en œuvre de la solution complète.

Créez une nouvelle sous-classe pour votre bouton (ou tout contrôle)

#import <UIKit/UIKit.h>

@interface MNGButton : UIButton

@end

Remplacez ensuite la méthode pointInside dans votre implémentation de sous-classe

@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

Sur votre fichier storyboard / xib, sélectionnez le contrôle en question et ouvrez l'inspecteur d'identité et tapez le nom de votre classe personnalisée.

bouton mng

Dans votre classe UIViewController pour la scène contenant le bouton, remplacez le type de classe du bouton par le nom de votre sous-classe.

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

Liez votre bouton storyboard / xib à la propriété IBOutlet et votre zone tactile sera étendue pour s'adapter à la zone définie dans la sous-classe.

En plus de remplacer la méthode pointInside avec les méthodes CGRectInset et CGRectContainsPoint , il faut prendre le temps d'examiner la CGGeometry pour étendre la zone tactile rectangulaire de toute sous-classe UIView. Vous pouvez également trouver de bons conseils sur les cas d'utilisation de CGGeometry sur NSHipster .

Par exemple, on pourrait rendre la zone tactile irrégulière en utilisant les méthodes mentionnées ci-dessus ou simplement choisir de rendre la zone tactile de largeur deux fois plus grande que la zone tactile horizontale:

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

NB: la substitution de n'importe quel contrôle de classe d'interface utilisateur devrait produire des résultats similaires en étendant la zone tactile pour différents contrôles (ou toute sous-classe UIView, comme UIImageView, etc.).

Tommie C.
la source
10

Cela fonctionne pour moi:

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
la source
3
Cela fonctionne, mais soyez prudent si vous devez définir à la fois une image et un titre sur un bouton. Dans ce cas, vous devrez utiliser setBackgoundImage qui mettra à l'échelle votre image au nouveau cadre du bouton.
Mihai Damian
7

Ne changez pas le comportement d'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
la source
Si votre bouton est 40x40, cette méthode ne sera pas appelée si les nombres négatifs sont plus grands - comme quelqu'un d'autre l'a mentionné. Sinon, cela fonctionne.
James O'Brien
Cela ne fonctionne pas pour moi. hitFrame semble calculé correctement, mais pointInside: n'est jamais appelé lorsque le point est en dehors de self.bounds. if (CGRectContainsPoint (hitFrame, point) &&! CGRectContainsPoint (self.bounds, point)) {NSLog (@ "Success!"); // Jamais appelé}
arsenius
7

J'utilise une approche plus générique en swizzling -[UIView pointInside:withEvent:]. Cela me permet de modifier le comportement de test de recherche sur tout UIView, pas seulementUIButton .

Souvent, un bouton est placé dans une vue de conteneur qui limite également le test de positionnement. Par exemple, lorsqu'un bouton se trouve en haut d'une vue de conteneur et que vous souhaitez étendre la cible tactile vers le haut, vous devez également étendre la cible tactile de la vue de conteneur.

@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

La bonne chose à propos de cette approche est que vous pouvez l'utiliser même dans les storyboards en ajoutant un attribut d'exécution défini par l'utilisateur. Malheureusement, UIEdgeInsetsn'est pas directement disponible en tant que type là , mais depuis se CGRectcompose également d'une struct avec quatre CGFloatil fonctionne parfaitement en choisissant « Rect » et remplir les valeurs comme ceci: {{top, left}, {bottom, right}}.

Ortwin Gentz
la source
6

J'utilise la classe suivante dans Swift, pour activer également une propriété Interface Builder pour ajuster la marge:

@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
la source
comment changer manuellement ce point à l'intérieur? Si j'ai créé un UIButton avec une ancienne marge, mais que je veux maintenant le changer?
TomSawyer
5

Eh bien, vous pouvez placer votre UIButton dans un UIView transparent et légèrement plus grand, puis capturer les événements tactiles sur l'instance UIView comme dans UIButton. De cette façon, vous aurez toujours votre bouton, mais avec une zone tactile plus grande. Vous devrez gérer manuellement les états sélectionnés et mis en évidence avec le bouton si l'utilisateur touche la vue au lieu du bouton.

Une autre possibilité consiste à utiliser une UIImage au lieu d'un UIButton.

Pablo Santa Cruz
la source
5

J'ai pu augmenter la zone de frappe du bouton info par programmation. Le graphique "i" ne change pas d'échelle et reste centré dans le nouveau cadre du bouton.

La taille du bouton info semble être fixée à 18x19 [*] dans Interface Builder. En le connectant à un IBOutlet, j'ai pu modifier sa taille d'image dans le code sans aucun problème.

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

[*]: Les versions ultérieures d'iOS semblent avoir augmenté la zone d'accès du bouton d'information.

Otto
la source
5

Ceci est ma solution Swift 3 (basée sur cet article de 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
la source
3

Le test de succès personnalisé de Chase implémenté en tant que sous-classe d'UIButton. Écrit en Objective-C.

Cela semble fonctionner à la fois pour initet pour les buttonWithType:constructeurs. Pour mes besoins, c'est parfait, mais comme le sous-classement UIButtonpeut être poilu, je serais intéressé de savoir si quelqu'un a un problème avec cela.

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
la source
3

Implémentation par remplacement hérité d'UIButton.

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
la source
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
la source
2

Ne remplacez jamais la méthode dans la catégorie. Bouton de sous-classe et remplacement - pointInside:withEvent:. Par exemple, si le côté de votre bouton est inférieur à 44 px (ce qui est recommandé comme zone d'appui minimale), utilisez ceci:

- (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
la source
2

J'ai créé une bibliothèque dans ce but précis.

Vous pouvez choisir d'utiliser une UIViewcatégorie, aucune sous-classification n'est requise :

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

Ou vous pouvez sous-classer votre UIView ou UIButton et définir le minimumHitTestWidthet / ouminimumHitTestHeight . Votre zone de test de frappe de bouton sera alors représentée par ces 2 valeurs.

Tout comme les autres solutions, il utilise la - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)eventméthode. La méthode est appelée lorsque iOS effectue un test de positionnement. Cet article de blog a une bonne description du fonctionnement des tests de succès iOS.

https://github.com/kgaidis/KGHitTestingViews

@interface KGHitTestingButton : UIButton <KGHitTesting>

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

@end

Vous pouvez également simplement sous-classer et utiliser Interface Builder sans écrire de code: entrez la description de l'image ici

kgaidis
la source
1
J'ai fini par installer ceci juste pour des raisons de vitesse. L'ajout d'IBInspectable était une excellente idée, merci d'avoir mis cela ensemble.
user3344977
2

Sur la base de la réponse de giaset ci-dessus (dont j'ai trouvé la solution la plus élégante), voici la version swift 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
la source
2

C'est aussi simple que ça:

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

C'est tout ça.

Fattie
la source
1

J'utilise cette astuce pour le bouton à l'intérieur de tableviewcell.accessoryView pour agrandir sa zone tactile

#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
la source
1

Je viens de faire le portage de la solution @Chase dans 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)
    }
}

un vous pouvez utiliser comme ça

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

Pour toute autre référence, voir https://stackoverflow.com/a/13067285/1728552

Serluca
la source
1

Réponse de @ antoine formatée pour 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
la source
0

J'ai suivi la réponse de Chase et cela fonctionne très bien, un seul problème lorsque vous créez une arrea trop grande, plus grande que la zone où le bouton est désélectionné (si la zone n'était pas plus grande), il n'appelle pas le sélecteur pour l'événement UIControlEventTouchUpInside .

Je pense que la taille est supérieure à 200 dans n'importe quelle direction ou quelque chose comme ça.

Dragon irréel
la source
0

Je suis si en retard à ce jeu, mais je voulais peser sur une technique simple qui pourrait résoudre vos problèmes. Voici un extrait de code UIButton programmatique typique pour moi:

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

Je charge une image png transparente pour mon bouton et je règle l'image d'arrière-plan. Je suis en train de définir le cadre en fonction de l'UIImage et de le mettre à l'échelle de 50% pour la rétine. OK, peut-être êtes-vous d'accord avec ce qui précède ou non, MAIS si vous voulez agrandir la zone de frappe et vous épargner un mal de tête:

Ce que je fais, ouvrez l'image dans Photoshop et augmentez simplement la taille de la toile à 120% et économisez. En fait, vous venez d'agrandir l'image avec des pixels transparents.

Juste une approche.

Chrisallick
la source
0

La réponse de @jlajlar ci-dessus semblait bonne et simple mais ne correspond pas à Xamarin.iOS, je l'ai donc convertie en Xamarin. Si vous recherchez une solution sur un iOS Xamarin, la voici:

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

Vous pouvez ajouter cette méthode à la classe dans laquelle vous substituez UIView ou UIImageView. Cela a bien fonctionné :)

A AlTaiar
la source
0

Aucune des réponses ne fonctionne parfaitement pour moi, car j'utilise une image d'arrière-plan et un titre sur ce bouton. De plus, le bouton se redimensionnera à mesure que la taille de l'écran change.

Au lieu de cela, j'agrandit la zone du robinet en agrandissant la zone transparente png.

Jakehao
la source