UIButton: Делает область попадания больше, чем область попадания по умолчанию

У меня есть вопрос, касающийся UIButton и его области применения. Я использую кнопку Info Dark в конструкторе интерфейсов, но обнаружил, что область попадания недостаточно велика для пальцев некоторых людей.

Есть ли способ увеличить область нажатия кнопки программно или в Interface Builder без изменения размера изображения InfoButton?


person Kevin Bomberry    schedule 30.04.2009    source источник
comment
Если ничего не работает, просто добавьте UIButton без изображения, а затем поместите оверлей ImageView!   -  person Varun    schedule 20.03.2017
comment
Это должен быть самый простой вопрос на всем сайте, на который есть десятки ответов. То есть, я могу вставить сюда весь ответ: override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { return bounds.insetBy(dx: -20, dy: -20).contains(point) }   -  person Fattie    schedule 22.09.2019


Ответы (35)


Поскольку я использую фоновое изображение, ни одно из этих решений не помогло мне. Вот решение, которое творит забавную магию objective-c и предлагает простое решение с минимальным кодом.

Сначала добавьте категорию в UIButton, которая отменяет проверку попадания, а также добавляет свойство для расширения кадра проверки попадания.

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

После добавления этого класса все, что вам нужно сделать, это установить вставки краев вашей кнопки. Обратите внимание, что я решил добавить вставки, поэтому, если вы хотите увеличить область попадания, вы должны использовать отрицательные числа.

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

Примечание. Не забудьте импортировать категорию (#import "UIButton+Extensions.h") в свои классы.

person Chase    schedule 25.10.2012
comment
Заменять метод в категории - плохая идея. Что, если другая категория также попытается переопределить pointInside: withEvent :? И вызов [super pointInside: withEvent] вызывает не версию вызова UIButton (если она есть), а версию родительского UIButton. - person honus; 19.07.2013
comment
@honus Вы правы, Apple не рекомендует этого делать. При этом, если этот код находится не в общей библиотеке, а только локально для вашего приложения, все должно быть в порядке. - person Chase; 19.07.2013
comment
Привет, Чейз, есть ли недостатки в использовании вместо этого подкласса? - person Sid; 13.01.2014
comment
@Sid, создание подкласса UIButton сложно, потому что инициализатор является статическим методом. Добавление методов с категорией означает, что вам не нужно явно использовать свой собственный класс, что может быть удобно. - person Chase; 14.01.2014
comment
Вы делаете слишком много ненужной работы, вы можете сделать две простые строки кода: [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"arrow.png"] forState:UIControlStateNormal]; И просто установить ширину и высоту, которые вам нужны: `backButton.frame = CGRectMake (5, 28, 45, 30);` - person Resty; 28.03.2014
comment
Мне нравится этот подход, он работает как шарм. Спасибо - person Wim Fikkert; 17.06.2014
comment
@Resty, он использует фоновое изображение, а это значит, что ваш подход не сработает. - person mattsson; 01.07.2014
comment
@Chase Хотя ваше утверждение о том, что код не находится в общей библиотеке, теоретически верно, если Apple однажды решит реализовать свой код в категории, все равно нет гарантии, кто из вас выиграет. Использование категорий для переопределения материала - это а) плохой стиль и б) ненадежность. И это как раз то, для чего нужны подклассы. Измените это, чтобы использовать подкласс. - person uliwitness; 30.04.2016
comment
@uliwitness Я не согласен. Этот вид заплатки обезьяны не идеален. К сожалению, у меня нет решения подкласса. Не стесняйтесь предоставить один. - person Chase; 02.05.2016
comment
Я не осознавал, насколько это крутое решение, пока не углубился в код еще больше. Затем, поскольку речь шла о создании подклассов, я попробовал свои силы в реализации подкласса - stackoverflow.com/a/37866101/4403600. - person Phillip Martin; 16.06.2016
comment
Ты спас мне день! Так элегантно! Спасибо! - person Tim Friedland; 09.05.2017

Просто установите значения вставки краев изображения в конструкторе интерфейсов.

person Samuel Goodwin    schedule 22.04.2011
comment
Я думал, что это не сработает для меня, потому что фоновые изображения не реагируют на вставки, но я изменил настройку свойства изображения, а затем установил отрицательную вставку левого края для моего заголовка шириной изображения, и он снова оказался поверх мой образ. - person Dave Ross; 24.04.2013
comment
Это также можно сделать в коде. Например, button.imageEdgeInsets = UIEdgeInsetsMake(-10, -10, -10, -10); - person bengoesboom; 01.11.2013
comment
Ответ Дэйва Росс великолепен. Для тех, кто не может это представить, -image перемещает ваш заголовок вправо от себя (по-видимому), поэтому вы можете щелкнуть маленький кликер в IB, чтобы переместить его назад (или в коде, как было сказано). Единственный минус, который я вижу, это то, что я не могу использовать растягиваемые изображения. Маленькая цена, которую нужно заплатить ИМО - person Paul Bruneau; 26.02.2014
comment
как это сделать, не растягивая изображение? - person zonabi; 12.07.2014
comment
Установите для режима содержимого что-то вроде «Центр», а не «Масштабирование». - person Samuel Goodwin; 01.11.2014
comment
Обновление для Xcode 8.1. Ошибка предварительной визуализации на вставках при использовании раскадровки. stackoverflow.com/a/40570294/1363634 - person CoderPug; 13.11.2016

Вот элегантное решение с использованием расширений в Swift. Это дает всем UIButton область действия размером не менее 44x44 точек в соответствии с рекомендациями 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
    }
}
person giaset    schedule 29.12.2014
comment
Вам нужно проверить, скрыта ли кнопка в настоящее время. Если да, верните ноль. - person Josh Gafni; 10.09.2015
comment
Спасибо, выглядит отлично. Есть ли какие-либо потенциальные недостатки, о которых следует знать? - person Crashalot; 06.04.2016
comment
Мне нравится это решение, не требует установки дополнительных свойств для всех кнопок. Спасибо - person xtrinch; 29.04.2016
comment
Обратной стороной является то, что если UIButton скрыт, это все равно вернет true! Обязательно посмотрите, скрыта кнопка или нет. - person Crashalot; 21.06.2016
comment
Если руководящие принципы Apple рекомендуют эти размеры для пораженной области, почему мы должны исправлять их невыполнение? Будет ли это решено в более поздних SDK? - person NoodleOfDeath; 06.09.2016
comment
Слава богу, у нас есть Stack Overflow и его участники. Потому что мы, конечно же, не можем полагаться на Apple в плане полезной, своевременной и точной информации о том, как решать их наиболее распространенные и базовые проблемы. - person Womble; 08.09.2016
comment
Действительно красивое решение! - person Dmitry; 16.11.2016
comment
Я просто добавляю это в подкласс UIButton и использую. Спасибо! - person miss Gbot; 15.08.2017

Вы также можете создать подкласс UIButton или пользовательский UIView и переопределить 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)
}

Цель-C

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    CGFloat margin = 5.0;
    CGRect area = CGRectInset(self.bounds, -margin, -margin);
    return CGRectContainsPoint(area, point);
}
person jlajlar    schedule 20.12.2012
comment
спасибо действительно классное решение без лишних категорий. Также я просто интегрирую его с моими кнопками, которые у меня есть в XIB, и он работает хорошо. отличный ответ - person Matrosov Alexander; 09.04.2013
comment
Это отличное решение, которое можно распространить на все остальные элементы управления! Например, я счел полезным создать подкласс UISearchBar. Метод pointInside присутствует в каждом UIView начиная с iOS 2.0. Кстати - Swift: func pointInside (_ point: CGPoint, withEvent event: UIEvent!) - ›Bool. - person Tommie C.; 08.09.2014
comment
Спасибо за это! Как уже говорилось, это может быть действительно полезно с другими элементами управления! - person Yanchi; 17.02.2015
comment
Отлично!! Спасибо, вы сэкономили мне много времени! :-) - person Vojta; 28.10.2016

Вот UIButton + Extensions Чейза в 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)
    }
}

Чтобы использовать его, вы можете:

button.touchAreaEdgeInsets = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
person dcRay    schedule 14.08.2015
comment
Фактически, мы должны расширять UIControl, а не UIButton. - person Valentin Shergin; 11.08.2016
comment
И переменная pTouchAreaEdgeInsets должна быть Int, а не UIEdgeInsets. (На самом деле, это может быть любой тип, но Int - наиболее общий и простой тип, поэтому он часто встречается в таких конструкциях). (Поэтому мне в целом нравится этот подход. Спасибо, dcRay!) - person Valentin Shergin; 11.08.2016
comment
Я пробовал titleEdgeInsets, imageEdgeInsets, contentEdgeInset, все они у меня не работают, это расширение работает хорошо. Спасибо за публикацию !! Прекрасная работа !! - person Yogesh Patel; 13.05.2019

Я рекомендую разместить UIButton с типом Custom по центру над вашей информационной кнопкой. Измените размер настраиваемой кнопки до желаемого размера области нажатия. Оттуда у вас есть два варианта:

  1. Установите флажок "Показывать касание при выделении" настраиваемой кнопки. Над кнопкой информации появится белое свечение, но в большинстве случаев палец пользователя будет закрывать его, и все, что они увидят, - это свечение снаружи.

  2. Настройте IBOutlet для информационной кнопки и два IBActions для настраиваемой кнопки: одно для «Touch Down» и одно для «Touch Up Inside». Затем в Xcode сделайте так, чтобы событие касания установило для выделенного свойства информационной кнопки значение YES, а событие touchupinside установило для выделенного свойства значение NO.

person Kyle    schedule 04.05.2009

Не устанавливайте свойство backgroundImage с вашим изображением, установите свойство imageView. Кроме того, убедитесь, что для imageView.contentMode установлено значение UIViewContentModeCenter.

person Maurizio    schedule 06.11.2010
comment
В частности, установите для свойства contentMode кнопки значение UIViewContentModeCenter. Затем вы можете сделать рамку кнопки больше изображения. Работает на меня! - person arlomedia; 07.09.2012
comment
К вашему сведению, UIButton не уважает UIViewContentMode: stackoverflow.com/a/4503920/361247 - person Enrico Susatyo; 19.04.2013
comment
@EnricoSusatyo, извините, я пояснил, о каких API я говорил. imageView.contentMode можно установить как UIContentModeCenter. - person Maurizio; 19.04.2013
comment
Работает отлично, спасибо за совет! [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage:[UIImage imageNamed:@"image.png"] forState:UIControlStateNormal]; - person Resty; 27.03.2014
comment
@Resty Вышеупомянутый комментарий не работает для меня: / Размер изображения изменяется до большего установленного размера кадра. - person Anil; 17.05.2014
comment
@Anil, хорошо, я постараюсь дать более подробную информацию, это работает, когда вы устанавливаете больше Width & Height в CGRectMake, поэтому сделайте вашу кнопку немного больше, я рекомендую вам сделайте это с помощью собственного изображения, вот мой полный рабочий пример: `UIButton * backButton = [UIButton buttonWithType: UIButtonTypeCustom]; backButton.frame = CGRectMake (100, 100, 45, 35); [[backButton imageView] setContentMode: UIViewContentModeCenter]; [backButton setImage: [UIImage imageNamed: @ arrow.png] forState: UIControlStateNormal]; // размер arror.png 19x34 [self.view addSubview: backButton]; // область попадания 45x35` - person Resty; 19.05.2014
comment
Я попытался сделать то же самое, но не смог заставить его работать. Наконец, переопределил UIButton и изменил его при помощи hitTest. Теперь он работает нормально. - person Anil; 19.05.2014

Мое решение на 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)
    }
}
person Zhanserik    schedule 01.08.2017

В представленных ответах нет ничего плохого; однако я хотел расширить jlarjlar's answer, поскольку он обладает удивительным потенциалом, который может повысить ценность той же проблемы с другими элементами управления (например, SearchBar). Это связано с тем, что, поскольку pointInside прикреплен к UIView, можно создать подкласс любой элемент управления для улучшения сенсорной области. Этот ответ также показывает полный пример того, как реализовать полное решение.

Создайте новый подкласс для вашей кнопки (или любого элемента управления)

#import <UIKit/UIKit.h>

@interface MNGButton : UIButton

@end

Затем переопределите метод pointInside в реализации вашего подкласса.

@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

В файле раскадровки / xib выберите соответствующий элемент управления, откройте инспектор удостоверений и введите имя своего настраиваемого класса.

mngbutton

В вашем классе UIViewController для сцены, содержащей кнопку, измените тип класса для кнопки на имя вашего подкласса.

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

Свяжите кнопку раскадровки / xib со свойством IBOutlet, и ваша сенсорная область будет расширена, чтобы соответствовать области, определенной в подклассе.

Помимо переопределения метод pointInside вместе с CGRectInset и CGRectContainsPoint, необходимо время, чтобы изучить CGGeometry для расширения прямоугольной сенсорной области любого подкласса UIView. Вы также можете найти несколько полезных советов по вариантам использования CGGeometry на странице NSHipster.

Например, можно сделать область касания нерегулярной, используя методы, упомянутые выше, или просто сделать ширину области касания в два раза больше, чем горизонтальная область касания:

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

NB: замена любого элемента управления UI Class должна привести к аналогичным результатам при расширении сенсорной области для различных элементов управления (или любого подкласса UIView, такого как UIImageView и т. Д.).

person Tommie C.    schedule 08.09.2014

Это работает для меня:

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)];
person Olof    schedule 12.05.2010
comment
Это работает, но будьте осторожны, если вам нужно установить для кнопки и изображение, и заголовок. В этом случае вам нужно будет использовать setBackgoundImage, который масштабирует ваше изображение до нового кадра кнопки. - person Mihai Damian; 24.08.2010

Не меняйте поведение 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
person user3109079    schedule 16.12.2013
comment
Если размер вашей кнопки 40x40, этот метод не будет вызываться, если отрицательные числа больше - как уже упоминалось кем-то другим. В противном случае это работает. - person James O'Brien; 13.05.2014
comment
У меня это не работает. hitFrame вычисляется правильно, но pointInside: никогда не вызывается, когда точка находится за пределами self.bounds. если (CGRectContainsPoint (hitFrame, point) &&! CGRectContainsPoint (self.bounds, point)) {NSLog (@Success!); // Никогда не звонили} - person arsenius; 28.10.2014

Я использую более общий подход, обмениваясь -[UIView pointInside:withEvent:]. Это позволяет мне изменять поведение проверки попадания для любого UIView, а не только для UIButton.

Часто кнопка помещается внутри представления контейнера, что также ограничивает проверку нажатия. Например, когда кнопка находится наверху представления контейнера, и вы хотите расширить цель касания вверх, вам также необходимо расширить цель касания представления контейнера.

@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

Хорошая вещь в этом подходе заключается в том, что вы можете использовать его даже в раскадровках, добавив определяемый пользователем атрибут времени выполнения. К сожалению, UIEdgeInsets не доступен напрямую как тип, но поскольку CGRect также состоит из структуры с четырьмя CGFloat, он работает безупречно, выбирая «Rect» и заполняя значения, подобные этому: {{top, left}, {bottom, right}}.

person Ortwin Gentz    schedule 20.11.2012

Я использую следующий класс в Swift, чтобы также включить свойство Interface Builder для регулировки поля:

@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)
    }
}
person Antoine    schedule 23.06.2015
comment
как вручную поменять этот пункт внутри? Если я создал UIButton со старым полем, но теперь хочу его изменить? - person TomSawyer; 12.10.2015

Что ж, вы можете разместить свой UIButton внутри прозрачного и немного большего UIView, а затем улавливать события касания в экземпляре UIView, как в UIButton. Таким образом, у вас все еще будет кнопка, но с большей площадью касания. Вам придется вручную обрабатывать выбранные и выделенные состояния с помощью кнопки, если пользователь касается представления вместо кнопки.

Другая возможность включает использование UIImage вместо UIButton.

person Pablo Santa Cruz    schedule 30.04.2009

Мне удалось программно увеличить область нажатия кнопки с информацией. Графика «i» не меняет масштаб и остается по центру в новой рамке кнопки.

Размер информационной кнопки, кажется, установлен на 18x19 [*] в Интерфейсном Разработчике. Подключив его к IBOutlet, я смог без проблем изменить его размер кадра в коде.

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

[*]: Более поздние версии iOS, похоже, увеличили область нажатия кнопки информации.

person otto    schedule 13.10.2009

Это мое Swift 3 решение (на основе этого сообщения в блоге: 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
    }
}
person arthurschiller    schedule 20.01.2017

Пользовательский тест попадания Чейза реализован как подкласс UIButton. Написано на Objective-C.

Кажется, работает как для init, так и для buttonWithType: конструкторов. Для моих нужд он идеален, но поскольку создание подклассов UIButton может быть непростым делом, мне было бы интересно узнать, есть ли в нем какие-либо недостатки.

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
person Phillip Martin    schedule 16.06.2016

Реализация путем переопределения унаследованного 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)
    }
}
person dimpiax    schedule 17.06.2016

Это просто:

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

Вот и все.

person Fattie    schedule 22.09.2019
comment
Хороший. Отличное решение. Спасибо! - person Yogesh Patel; 10.03.2021
comment
ура @YogeshPatel. Да, есть досадная проблема с примером кода iOS - он очень быстро УСТАРЕВЛЯЕТСЯ. У вас возникают вопросы о SO, где верхний / отмеченный ответ действительно невероятно неправильный и устаревший! - person Fattie; 10.03.2021

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
person William Jockusch    schedule 03.08.2012

Никогда не переопределяйте метод в категории. Кнопка подкласса и переопределение - pointInside:withEvent:. Например, если сторона вашей кнопки меньше 44 пикселей (что рекомендуется в качестве минимальной области нажатия), используйте это:

- (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));
}
person user500    schedule 06.06.2014

Я сделал библиотеку именно для этой цели.

Вы можете использовать UIView категорию, подклассы не требуются:

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

Или вы можете создать подкласс своего UIView или UIButton и установить minimumHitTestWidth и / или minimumHitTestHeight. Тогда ваша область проверки нажатия будет представлена ​​этими двумя значениями.

Как и в других решениях, он использует метод - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event. Метод вызывается, когда iOS выполняет проверку попадания. В этом сообщении блога есть хорошее описание того, как работает проверка попаданий в iOS.

https://github.com/kgaidis/KGHitTestingViews

@interface KGHitTestingButton : UIButton <KGHitTesting>

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

@end

Вы также можете создать подкласс и использовать Interface Builder без написания кода: введите описание изображения здесь

person kgaidis    schedule 17.05.2015
comment
В итоге я установил это просто ради скорости. Добавление IBInspectable было отличной идеей, спасибо за то, что собрали все вместе. - person user3344977; 18.01.2016

Основываясь на ответе giaset выше (который я нашел наиболее элегантное решение), вот версия быстрой 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
    }
}
person Thanh-Nhon Nguyen    schedule 16.09.2016

Я использую этот трюк для кнопки внутри tableviewcell.accessoryView, чтобы увеличить область касания

#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];
}
person abuharsky    schedule 08.08.2012

Я только что сделал порт решения @Chase в быстрой версии 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)
    }
}

и вы можете использовать это так

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

Для любой другой ссылки см. https://stackoverflow.com/a/13067285/1728552.

person Serluca    schedule 03.08.2016

Подобно Zhanserik, с расширением переменной и обновленным для Swift 4.2:

class ButtonWithExtendedHitArea: UIButton {

    var extention: CGFloat

    required init(extendBy: CGFloat) {
        extention = extendBy

        super.init(frame: .zero)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override open func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let relativeFrame = self.bounds
        let hitTestEdgeInsets = UIEdgeInsets(top: -extention, left: -extention, bottom: -extention, right: -extention)
        let hitFrame = relativeFrame.inset(by: hitTestEdgeInsets)
        return hitFrame.contains(point)
    }

}
person MT1asli8ghwoi    schedule 22.02.2019
comment
из всех предложенных вариантов этот вариант у меня работает на 100% - person PAULMAX; 30.04.2021

Ответ @antoine, отформатированный для 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 );
    }
}
person spfursich    schedule 18.03.2019

Я вижу много решений, которые либо не совсем подходят, либо требуют добавления некоторых фиксированных вставок. Вот решение для простого подкласса UIView, который расширит область действия представления до как минимум 44 x 44; если какое-либо из измерений уже больше этого, то это измерение не дополняется искусственно.

Это гарантирует, что кнопка всегда будет иметь рекомендуемый минимальный размер касания 44 x 44 без необходимости ручной настройки, расчета или заполнения изображения:

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    let minimumTouchSize: CGFloat = 44.0
    let center: CGPoint = .init(x: self.bounds.midX, y: self.bounds.midY)

    let minimumHitRect: CGRect =
        .init(center: center, size: .zero)
        .insetBy(
            dx: -minimumTouchSize / 2.0,
            dy: -minimumTouchSize / 2.0
        )

    let fullHitRect = self.bounds.union(minimumHitRect)

    return fullHitRect.contains(point)
}
person CIFilter    schedule 10.06.2020
comment
Если не считать того, что Apple разработала лучшее решение, это кажется наиболее универсальным и надежным ответом с наименьшим количеством побочных эффектов. Я думаю, что принудительное согласие через подкласс намного безопаснее. Моя единственная жалоба на большинство этих обходных путей заключается в том, что они неочевидны для младших разработчиков и потенциально могут стать источником путаницы при отладке, но у меня нет лучших простых решений, которые я могу предложить! - person MechEthan; 08.04.2021

Я следил за ответом Чейза, и он отлично работает, одна проблема, когда вы создаете слишком большой arrea, больше, чем зона, в которой кнопка отменяется (если зона не была больше), он не вызывает селектор для события UIControlEventTouchUpInside .

Я думаю, что размер больше 200 в любом направлении или что-то в этом роде.

person Unreal Dragon    schedule 12.04.2013

Я так поздно пришел к этой игре, но хотел взвесить простую технику, которая могла бы решить ваши проблемы. Вот типичный для меня программный фрагмент UIButton:

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

Я загружаю прозрачное png-изображение для своей кнопки и устанавливаю фоновое изображение. Я устанавливаю рамку на основе UIImage и масштабирую на 50% для сетчатки. Хорошо, может быть, вы согласны с вышеизложенным или нет, НО если вы хотите сделать зону попадания БОЛЬШЕЙ и избавить себя от головной боли:

Что я делаю, открываю изображение в фотошопе, просто увеличиваю размер холста до 120% и сохраняю. Фактически вы только что увеличили изображение с помощью прозрачных пикселей.

Всего один подход.

person chrisallick    schedule 06.11.2014

Ответ @jlajlar выше казался хорошим и простым, но не соответствует Xamarin.iOS, поэтому я преобразовал его в Xamarin. Если вы ищете решение для Xamarin iOS, вот оно:

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

Вы можете добавить этот метод в класс, в котором вы переопределяете UIView или UIImageView. Это отлично сработало :)

person Has AlTaiar    schedule 20.03.2015

Ни один из ответов не подходит мне идеально, потому что я использую фоновое изображение и заголовок на этой кнопке. Кроме того, размер кнопки будет изменяться при изменении размера экрана.

Вместо этого я увеличиваю область касания, увеличивая прозрачную область png.

person Jakehao    schedule 13.04.2016

Эта версия Swift позволяет вам определять минимальный размер попадания для всех UIButton. Что особенно важно, он также обрабатывает случай, когда UIButtons скрыты, что многие ответы игнорируют.

extension UIButton {
    public override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
        // Ignore if button hidden
        if self.hidden {
            return nil
        }

        // If here, button visible so expand hit area
        let hitSize = CGFloat(56.0)
        let buttonSize = self.frame.size
        let widthToAdd = (hitSize - buttonSize.width > 0) ? hitSize - buttonSize.width : 0
        let heightToAdd = (hitSize - buttonSize.height > 0) ? hitSize - buttonSize.height : 0
        let largerFrame = CGRect(x: 0-(widthToAdd/2), y: 0-(heightToAdd/2), width: buttonSize.width+widthToAdd, height: buttonSize.height+heightToAdd)
        return (CGRectContainsPoint(largerFrame, point)) ? self : nil
    }
}
person Crashalot    schedule 21.06.2016

Быстрый:

override func viewWillAppear(animated: Bool) {
        self.sampleButton.frame = CGRectInset(self.sampleButton.frame, -10, -10);
    }
person A.G    schedule 20.07.2016

  • Без переопределения в категориях или расширениях
  • Минимум 44 x 44 в соответствии с рекомендациями

Мое мнение:

open class MinimumTouchAreaButton: UIButton {
    open override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard !self.isHidden, self.isUserInteractionEnabled, self.alpha > 0 else { return nil }
        let expandedBounds = bounds.insetBy(dx: min(bounds.width - 44, 0), dy: min(bounds.height - 44, 0))
        return expandedBounds.contains(point) ? self : nil
    }
} 
person Jacob Jennings    schedule 19.07.2017