UIButton: Vergrößert den Trefferbereich als Standard-Trefferbereich

188

Ich habe eine Frage zu UIButton und seinem Trefferbereich. Ich verwende die Schaltfläche "Info Dark" im Interface Builder, stelle jedoch fest, dass der Trefferbereich nicht groß genug für die Finger einiger Personen ist.

Gibt es eine Möglichkeit, die Trefferfläche einer Schaltfläche entweder programmgesteuert oder im Interface Builder zu vergrößern, ohne die Größe der InfoButton-Grafik zu ändern?

Kevin Bomberry
quelle
Wenn nichts funktioniert, fügen Sie einfach einen UIButton ohne Bild hinzu und fügen Sie dann ein ImageView-Overlay ein!
Varun
Dies müsste die erstaunlich einfachste Frage auf der gesamten Website sein, die Dutzende von Antworten enthält. Ich meine, ich kann die gesamte Antwort hier einfügen: override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return bounds.insetBy(dx: -20, dy: -20).contains(point) }
Fattie

Antworten:

137

Da ich ein Hintergrundbild verwende, hat keine dieser Lösungen für mich gut funktioniert. Hier ist eine Lösung, die Spaß macht und eine Drop-In-Lösung mit minimalem Code bietet.

Fügen Sie zunächst eine Kategorie hinzu UIButton, die den Treffer-Test überschreibt, und fügen Sie eine Eigenschaft zum Erweitern des Treffer-Test-Frames hinzu.

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

Sobald diese Klasse hinzugefügt wurde, müssen Sie nur noch die Kanteneinfügungen Ihrer Schaltfläche festlegen. Beachten Sie, dass ich die Einfügungen hinzugefügt habe. Wenn Sie also die Trefferfläche vergrößern möchten, müssen Sie negative Zahlen verwenden.

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

Hinweis: Denken Sie daran, die Kategorie ( #import "UIButton+Extensions.h") in Ihre Klassen zu importieren .

Verfolgungsjagd
quelle
26
Das Überschreiben einer Methode in einer Kategorie ist eine schlechte Idee. Was ist, wenn eine andere Kategorie ebenfalls versucht, pointInside: withEvent: zu überschreiben? Und der Aufruf von [super pointInside: withEvent] ruft nicht die UIButton-Version des Aufrufs (falls vorhanden) auf, sondern die übergeordnete Version von UIButton.
Honus
@honus Sie haben Recht und Apple empfiehlt dies nicht. Davon abgesehen sollte es Ihnen gut gehen, solange sich dieser Code nicht in einer gemeinsam genutzten Bibliothek befindet und nur lokal für Ihre Anwendung ist.
Chase
Hallo Chase, gibt es einen Nachteil bei der Verwendung einer Unterklasse?
Sid
3
[[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"arrow.png"] forState:UIControlStateNormal];Wenn Sie zu viel unnötige Arbeit leisten, können Sie zwei einfache Codezeilen ausführen: Stellen Sie einfach die Breite und Höhe ein, die Sie benötigen: `backButton.frame = CGRectMake (5, 28, 45, 30);`
Resty
7
@Resty, er verwendet das Hintergrundbild, was bedeutet, dass Ihr Ansatz nicht funktioniert.
Mattsson
77

Legen Sie einfach die Werte für die Bildkanteneinfügung im Interface Builder fest.

Samuel Goodwin
quelle
1
Ich dachte, das würde bei mir nicht funktionieren, da Hintergrundbilder nicht auf die Einfügungen reagieren, aber ich habe die Bildeigenschaft festgelegt und dann für meinen Titel einen negativen linken Randeinsatz festgelegt, der die Breite des Bildes und das Bild wieder überlagert mein Bild.
Dave Ross
12
Dies kann auch im Code erfolgen. Zum Beispielbutton.imageEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10);
bengoesboom
Dave Ross 'Antwort ist großartig. Für diejenigen, die es sich nicht vorstellen können, stößt das Bild Ihren Titel (anscheinend) rechts von sich an, sodass Sie auf den kleinen Clicker in IB klicken können, um ihn zurückzuschieben (oder im Code, wie angegeben). Das einzige Negative, das ich sehe, ist, dass ich keine dehnbaren Bilder verwenden kann. Kleiner Preis zu zahlen IMO
Paul Bruneau
7
Wie geht das, ohne das Bild zu dehnen?
Zonabi
Stellen Sie den Inhaltsmodus auf "Mitte" und nicht auf "Skalieren".
Samuel Goodwin
63

Hier ist eine elegante Lösung mit Extensions in Swift. Es gibt allen UIButtons einen Trefferbereich von mindestens 44 x 44 Punkten gemäß den Richtlinien für die Benutzeroberfläche von 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
quelle
3
Sie müssen testen, ob die Schaltfläche derzeit ausgeblendet ist. Wenn ja, geben Sie nil zurück.
Josh Gafni
Danke, das sieht gut aus. Mögliche Nachteile, die Sie beachten sollten?
Crashalot
Ich mag diese Lösung, ich muss keine zusätzlichen Eigenschaften für alle Schaltflächen festlegen. Vielen Dank
Xtrinch
1
Wenn die Apple-Richtlinien diese Dimensionen für den Trefferbereich empfehlen, warum müssen wir dann den Fehler bei der Implementierung beheben? Wird dies in späteren SDKs behandelt?
NoodleOfDeath
2
Gott sei Dank haben wir Stack Overflow und seine Mitwirkenden. Weil wir uns sicherlich nicht auf Apple verlassen können, um nützliche, zeitnahe und genaue Informationen zur Behebung der häufigsten grundlegenden Probleme zu erhalten.
Womble
49

Sie können auch eine Unterklasse UIButtonoder einen Benutzer UIViewfestlegen und Folgendes überschreiben point(inside:with:):

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

Ziel c

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGFloat margin = 5.0;
    CGRect area = CGRectInset(self.bounds, -margin, -margin);
    return CGRectContainsPoint(area, point);
}
jlajlar
quelle
1
danke wirklich coole lösung ohne zusätzliche kategorien. Außerdem integriere ich es einfach in meine Schaltflächen, die ich in XIB habe, und es funktioniert gut. tolle Antwort
Matrosov Alexander
Dies ist eine großartige Lösung, die auf alle anderen Steuerelemente erweitert werden kann! Ich fand es nützlich, zum Beispiel eine UISearchBar zu unterordnen. Die pointInside-Methode ist seit iOS 2.0 in jedem UIView verfügbar. Übrigens - Swift: func pointInside (_ point: CGPoint, withEvent event: UIEvent!) -> Bool.
Tommie C.
Danke dafür! Wie bereits erwähnt, kann dies bei anderen Steuerelementen sehr nützlich sein!
Yanchi
Das ist toll!! Danke, du hast mir viel Zeit gespart! :-)
Vojta
35

Hier ist Chases UIButton + Extensions in 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)
    }
}

Um es zu verwenden, können Sie:

button.touchAreaEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
dcRay
quelle
1
Eigentlich sollten wir verlängern UIControl, nicht UIButton.
Valentin Shergin
2
Und Variable pTouchAreaEdgeInsetsmuss Intnicht sein UIEdgeInsets. (Eigentlich kann es jeder Typ sein, aber es Intist der allgemeinste und einfachste Typ, daher ist er bei solchen Konstruktionen üblich). (Deshalb liebe ich diesen Ansatz im Allgemeinen. Danke, dcRay!)
Valentin Shergin
1
Ich habe versucht, titleEdgeInsets, imageEdgeInsets, contentEdgeInset alle funktionieren nicht für mich. Diese Erweiterung funktioniert gut. Vielen Dank für die Veröffentlichung! Gut gemacht !!
Yogesh Patel
19

Ich empfehle, einen UIButton mit dem Typ Benutzerdefiniert über Ihrer Info-Schaltfläche zu platzieren. Ändern Sie die Größe der benutzerdefinierten Schaltfläche auf die Größe, die der Trefferbereich haben soll. Von dort haben Sie zwei Möglichkeiten:

  1. Aktivieren Sie die Option "Touch-on-Highlight anzeigen" der benutzerdefinierten Schaltfläche. Das weiße Leuchten erscheint über der Info-Schaltfläche, aber in den meisten Fällen deckt der Finger des Benutzers dies ab und alles, was er sieht, ist das Leuchten um die Außenseite.

  2. Richten Sie ein IBOutlet für die Info-Schaltfläche und zwei IBActions für die benutzerdefinierte Schaltfläche ein, eine für 'Touch Down' und eine für 'Touch Up Inside'. Stellen Sie dann in Xcode das Touchdown-Ereignis ein, setzen Sie die hervorgehobene Eigenschaft der Info-Schaltfläche auf YES, und das touchupinside-Ereignis setzt die hervorgehobene Eigenschaft auf NO.

Kyle
quelle
19

Legen Sie die backgroundImageEigenschaft nicht mit Ihrem Bild fest, sondern legen Sie sie fest imageView. Also, stellen Sie sicher , dass Sie imageView.contentModesetzen auf UIViewContentModeCenter.

Maurizio
quelle
1
Setzen Sie insbesondere die contentMode-Eigenschaft der Schaltfläche auf UIViewContentModeCenter. Dann können Sie den Rahmen der Schaltfläche größer als das Bild machen. Funktioniert bei mir!
Arlomedia
Zu Ihrer Information, UIButton respektiert UIViewContentMode nicht: stackoverflow.com/a/4503920/361247
Enrico Susatyo
@EnricoSusatyo Entschuldigung, ich habe klargestellt, über welche APIs ich gesprochen habe. Das imageView.contentModekann eingestellt werden als UIContentModeCenter.
Maurizio
Das funktioniert perfekt, danke für den Rat! [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"image.png"] forState:UIControlStateNormal];
Ruhe
@Resty Der oben erwähnte Kommentar funktioniert bei mir nicht: / Die Bildgröße wird auf die größere eingestellte Bildgröße geändert.
Anil
14

Meine Lösung für 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
quelle
12

An den Antworten ist nichts auszusetzen. Ich wollte jedoch die Antwort von jlarjlar erweitern, da sie ein erstaunliches Potenzial birgt, das das gleiche Problem mit anderen Steuerelementen (z. B. SearchBar) aufwerten kann. Dies liegt daran, dass pointInside an eine UIView angehängt ist und jedes Steuerelement in Unterklassen unterteilt werden kann , um den Berührungsbereich zu verbessern. Diese Antwort zeigt auch ein vollständiges Beispiel für die Implementierung der vollständigen Lösung.

Erstellen Sie eine neue Unterklasse für Ihre Schaltfläche (oder ein beliebiges Steuerelement).

#import <UIKit/UIKit.h>

@interface MNGButton : UIButton

@end

Überschreiben Sie als Nächstes die pointInside-Methode in Ihrer Unterklassenimplementierung

@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

Wählen Sie in Ihrer Storyboard- / XIB-Datei das betreffende Steuerelement aus, öffnen Sie den Identitätsinspektor und geben Sie den Namen Ihrer benutzerdefinierten Klasse ein.

mngbutton

Ändern Sie in Ihrer UIViewController-Klasse für Szenen, die die Schaltfläche enthalten, den Klassentyp für die Schaltfläche in den Namen Ihrer Unterklasse.

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

Verknüpfen Sie Ihre Storyboard- / XIB-Schaltfläche mit der Eigenschaft IBOutlet, und Ihr Touch-Bereich wird erweitert, um dem in der Unterklasse definierten Bereich zu entsprechen.

Zusätzlich zum Überschreiben der pointInside-Methode zusammen mit den Methoden CGRectInset und CGRectContainsPoint sollte man sich Zeit nehmen, um die CGGeometry auf die Erweiterung des rechteckigen Berührungsbereichs einer UIView-Unterklasse zu untersuchen. Möglicherweise finden Sie auch einige nützliche Tipps zu CGGeometry-Anwendungsfällen bei NSHipster .

Zum Beispiel könnte man den Berührungsbereich mit den oben genannten Methoden unregelmäßig machen oder einfach wählen, den Breiten-Berührungsbereich doppelt so groß wie den horizontalen Berührungsbereich zu machen:

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

Hinweis: Das Ersetzen eines UI-Klassensteuerelements sollte zu ähnlichen Ergebnissen führen, wenn der Berührungsbereich für verschiedene Steuerelemente (oder eine UIView-Unterklasse wie UIImageView usw.) erweitert wird.

Tommie C.
quelle
10

Das funktioniert bei mir:

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
quelle
3
Dies funktioniert, aber seien Sie vorsichtig, wenn Sie sowohl ein Bild als auch einen Titel auf eine Schaltfläche setzen müssen. In diesem Fall müssen Sie setBackgoundImage verwenden, um Ihr Bild auf den neuen Rahmen der Schaltfläche zu skalieren.
Mihai Damian
7

Ändern Sie nicht das Verhalten von 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
quelle
Wenn Ihre Schaltfläche 40 x 40 ist, wird diese Methode nicht aufgerufen, wenn die negativen Zahlen größer sind - wie bei einer anderen Person. Ansonsten funktioniert das.
James O'Brien
Das funktioniert bei mir nicht. hitFrame wird korrekt berechnet angezeigt, pointInside: wird jedoch nie aufgerufen, wenn der Punkt außerhalb von self.bounds liegt. if (CGRectContainsPoint (hitFrame, point) &&! CGRectContainsPoint (self.bounds, point)) {NSLog (@ "Success!"); // Nie angerufen}
Arsenius
7

Ich benutze einen allgemeineren Ansatz durch Swizzling -[UIView pointInside:withEvent:]. Auf diese Weise kann ich das Verhalten beim Testen von Treffern ändern UIView, nicht nur UIButton.

Oft wird eine Schaltfläche in einer Containeransicht platziert, die auch die Trefferprüfung einschränkt. Wenn sich beispielsweise eine Schaltfläche oben in einer Containeransicht befindet und Sie das Berührungsziel nach oben erweitern möchten, müssen Sie auch das Berührungsziel der Containeransicht erweitern.

@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

Das Schöne an diesem Ansatz ist, dass Sie ihn auch in Storyboards verwenden können, indem Sie ein benutzerdefiniertes Laufzeitattribut hinzufügen. Leider UIEdgeInsetsist es dort nicht direkt als Typ verfügbar, aber da es CGRectauch aus einer Struktur mit vier besteht CGFloat, funktioniert es einwandfrei, indem Sie "Rect" auswählen und die folgenden Werte eingeben : {{top, left}, {bottom, right}}.

Ortwin Gentz
quelle
6

Ich verwende die folgende Klasse in Swift, um auch eine Interface Builder-Eigenschaft zum Anpassen des Randes zu aktivieren:

@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
quelle
Wie kann man diesen Punkt manuell ändern? Wenn ich einen UIButton mit altem Rand erstellt habe, ihn aber jetzt ändern möchte?
TomSawyer
5

Nun, Sie können Ihren UIButton in einem transparenten und etwas größeren UIView platzieren und dann die Berührungsereignisse auf der UIView-Instanz wie im UIButton abfangen. Auf diese Weise haben Sie immer noch Ihre Schaltfläche, jedoch mit einem größeren Berührungsbereich. Sie müssen manuell mit ausgewählten und hervorgehobenen Zuständen auf der Schaltfläche umgehen, wenn der Benutzer die Ansicht anstelle der Schaltfläche berührt.

Eine andere Möglichkeit besteht darin, ein UIImage anstelle eines UIButton zu verwenden.

Pablo Santa Cruz
quelle
5

Ich konnte den Trefferbereich der Info-Schaltfläche programmgesteuert vergrößern. Die "i" -Grafik ändert den Maßstab nicht und bleibt im neuen Schaltflächenrahmen zentriert.

Die Größe der Info-Schaltfläche scheint im Interface Builder auf 18x19 [*] festgelegt zu sein. Durch die Verbindung mit einem IBOutlet konnte ich die Frame-Größe im Code ohne Probleme ändern.

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

[*]: Spätere Versionen von iOS scheinen den Trefferbereich der Info-Schaltfläche vergrößert zu haben.

otto
quelle
5

Dies ist meine Swift 3- Lösung (basierend auf diesem Blogpost: 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
quelle
3

Chases benutzerdefinierter Treffer-Test wurde als Unterklasse von UIButton implementiert. Geschrieben in Objective-C.

Es scheint sowohl für initals auch für buttonWithType:Konstrukteure zu funktionieren . Für meine Bedürfnisse ist es perfekt, aber da Unterklassen UIButtonhaarig sein können, würde mich interessieren, ob jemand einen Fehler daran hat.

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

Implementierung durch Überschreiben des geerbten 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
quelle
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
quelle
2

Überschreiben Sie niemals die Methode in der Kategorie. Unterklassenschaltfläche und Überschreiben - pointInside:withEvent:. Verwenden Sie beispielsweise Folgendes, wenn die Seite Ihrer Schaltfläche kleiner als 44 Pixel ist (was als Mindestabgriffsfläche empfohlen wird):

- (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
quelle
2

Zu diesem Zweck habe ich eine Bibliothek erstellt .

Sie können eine UIViewKategorie verwenden, ohne dass eine Unterklasse erforderlich ist :

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

Oder Sie können Ihre UIView oder UIButton in Unterklassen unterteilen und das minimumHitTestWidthund / oder festlegen minimumHitTestHeight. Ihr Button-Hit-Test-Bereich wird dann durch diese 2 Werte dargestellt.

Genau wie bei anderen Lösungen wird die - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)eventMethode verwendet. Die Methode wird aufgerufen, wenn iOS Treffer-Tests durchführt. Dieser Blog-Beitrag enthält eine gute Beschreibung der Funktionsweise von iOS-Hit-Tests.

https://github.com/kgaidis/KGHitTestingViews

@interface KGHitTestingButton : UIButton <KGHitTesting>

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

@end

Sie können den Interface Builder auch einfach unterordnen und verwenden, ohne Code zu schreiben: Geben Sie hier die Bildbeschreibung ein

kgaidis
quelle
1
Am Ende habe ich dies nur aus Gründen der Geschwindigkeit installiert. Das Hinzufügen von IBInspectable war eine großartige Idee, danke für die Zusammenstellung.
user3344977
2

Basierend auf der obigen Antwort von giaset (für die ich die eleganteste Lösung gefunden habe), hier ist die schnelle 3-Version:

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

So einfach ist das:

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

Das ist das Ganze.

Fattie
quelle
1

Ich verwende diesen Trick für die Schaltfläche in tableviewcell.accessoryView, um den Berührungsbereich zu vergrößern

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

Ich habe gerade den Port der @ Chase- Lösung in Swift 2.2 erstellt

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

und Sie können so verwenden

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

Weitere Informationen finden Sie unter https://stackoverflow.com/a/13067285/1728552

Serluca
quelle
1

@ antoines Antwort für Swift 4 formatiert

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
quelle
0

Ich habe Chases Antwort verfolgt und es funktioniert hervorragend. Ein einziges Problem, wenn Sie den Bereich zu groß erstellen, der größer als die Zone ist, in der die Schaltfläche abgewählt wird (wenn die Zone nicht größer ist), wird der Selektor für das UIControlEventTouchUpInside-Ereignis nicht aufgerufen .

Ich denke, die Größe ist über 200 in jede Richtung oder so ähnlich.

Unwirklicher Drache
quelle
0

Ich bin so spät zu diesem Spiel, wollte aber eine einfache Technik abwägen, die Ihre Probleme lösen könnte. Hier ist ein typisches programmatisches UIButton-Snippet für mich:

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

Ich lade ein transparentes PNG-Bild für meine Schaltfläche und stelle das Hintergrundbild ein. Ich stelle den Rahmen basierend auf dem UIImage ein und skaliere für die Netzhaut um 50%. OK, vielleicht stimmen Sie dem oben Gesagten zu oder nicht, ABER wenn Sie den Trefferbereich GRÖSSER machen und sich Kopfschmerzen ersparen möchten:

Was ich mache, öffne das Bild in Photoshop und erhöhe einfach die Leinwandgröße auf 120% und speichere. Tatsächlich haben Sie gerade das Bild mit transparenten Pixeln vergrößert.

Nur ein Ansatz.

Chrisallick
quelle
0

Die obige Antwort von @jlajlar schien gut und unkompliziert zu sein, stimmt jedoch nicht mit Xamarin.iOS überein, daher habe ich sie in Xamarin konvertiert. Wenn Sie nach einer Lösung für ein Xamarin iOS suchen, finden Sie hier:

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

Sie können diese Methode der Klasse hinzufügen, in der Sie UIView oder UIImageView überschreiben. Das hat gut funktioniert :)

Hat AlTaiar
quelle
0

Keine der Antworten funktioniert perfekt für mich, da ich ein Hintergrundbild und einen Titel auf dieser Schaltfläche verwende. Darüber hinaus wird die Größe der Schaltfläche geändert, wenn sich die Bildschirmgröße ändert.

Stattdessen vergrößere ich den Tap-Bereich, indem ich den transparenten PNG-Bereich vergrößere.

Jakehao
quelle