Как я могу нажать кнопку за прозрачным UIView?

177

Допустим, у нас есть контроллер представления с одним вложенным представлением. подпредставление занимает центр экрана с полями 100 пикселей со всех сторон. Затем мы добавляем кучу маленьких вещей, чтобы щелкнуть внутри этого подпредставления. Мы используем подпредставление только для того, чтобы воспользоваться новым кадром (x = 0, y = 0 внутри подпредставления на самом деле 100,100 в родительском представлении).

Затем представьте, что у нас есть что-то позади подпредставления, например, меню. Я хочу, чтобы пользователь мог выбрать любой «маленький материал» в подпредставлении, но если там ничего нет, я хочу, чтобы касания проходили через него (так как фон в любом случае ясен) к кнопкам позади него.

Как я могу это сделать? Похоже, прикосновения Беган проходит, но кнопки не работают.

Шон Кларк Хесс
источник
1
Я думал, прозрачные (альфа 0) UIViews не должны реагировать на сенсорные события?
Evadne Wu
1
Я написал небольшой класс только для этого. (Добавлен пример в ответах). Решение там несколько лучше, чем принятый ответ, потому что вы все равно можете щелкнуть по элементу, UIButtonкоторый находится под полупрозрачным, в UIViewто время как непрозрачная часть по- UIViewпрежнему будет реагировать на сенсорные события.
Сегев,

Ответы:

315

Создайте пользовательское представление для своего контейнера и переопределите сообщение pointInside: для возврата false, если точка не находится в допустимом дочернем представлении, например так:

Swift:

class PassThroughView: UIView {
    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        for subview in subviews {
            if !subview.isHidden && subview.isUserInteractionEnabled && subview.point(inside: convert(point, to: subview), with: event) {
                return true
            }
        }
        return false
    }
}

Цель C:

@interface PassthroughView : UIView
@end

@implementation PassthroughView
-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    for (UIView *view in self.subviews) {
        if (!view.hidden && view.userInteractionEnabled && [view pointInside:[self convertPoint:point toView:view] withEvent:event])
            return YES;
    }
    return NO;
}
@end

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

Джон Стивен
источник
4
Интересный. Мне нужно больше погрузиться в цепочку респондента.
Шон Кларк Хесс
1
вам нужно проверить, виден ли вид также, тогда вам также нужно проверить, видны ли подпредставления перед их тестированием.
Ленивый кодер
2
к сожалению, этот трюк не работает для tabBarController, для которого представление не может быть изменено. У кого-нибудь есть идея сделать этот взгляд прозрачным и для событий?
cyrilchampier
1
Вы также должны проверить альфа-представление. Довольно часто я скрываю вид, устанавливая альфа на ноль. Представление с нулевым альфа-каналом должно действовать как скрытое представление.
Джеймс Эндрюс
2
Я сделал быструю версию: возможно, вы можете включить ее в ответ для наглядности gist.github.com/eyeballz/17945454447c7ae766cb
eyeballz
107

Я также использую

myView.userInteractionEnabled = NO;

Нет необходимости в подклассе. Работает отлично.

AKW
источник
76
Это также отключит взаимодействие с пользователем для любых
подпредставлений
Как уже упоминалось @pixelfreak, это не идеальное решение для всех случаев. В тех случаях, когда взаимодействие в подпредставлениях этого представления все еще желательно, этот флаг должен оставаться включенным.
Аугусто Кармо
20

От Apple:

Пересылка событий - это метод, используемый некоторыми приложениями. Вы пересылаете события касания, вызывая методы обработки событий другого объекта респондента. Хотя это может быть эффективным методом, вы должны использовать его с осторожностью. Классы инфраструктуры UIKit не предназначены для получения касаний, которые не связаны с ними .... Если вы хотите условно пересылать касания другим респондентам в вашем приложении, все эти респонденты должны быть экземплярами ваших собственных подклассов UIView.

Лучшая практика яблок :

Не отправляйте явно события вверх по цепочке респондента (через nextResponder); вместо этого, вызовите реализацию суперкласса и позвольте UIKit обрабатывать обход цепочки респондента.

вместо этого вы можете переопределить:

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

в своем подклассе UIView и верните NO, если вы хотите, чтобы это касание было отправлено вверх по цепочке респондента (IE для просмотра за вашим представлением, в котором ничего нет).

jackslash
источник
1
Было бы здорово иметь ссылку на документы, которые вы цитируете, вместе с самими цитатами.
morgancodes
1
Я добавил ссылки на соответствующие места в документации для вас
jackslash
1
~~ Не могли бы вы показать, как должно выглядеть это переопределение? ~~ Нашли ответ в другом вопросе о том, как это будет выглядеть; это буквально только что вернулось NO;
Контур
Вероятно, это должен быть принятый ответ. Это самый простой и рекомендуемый способ.
Эквадор
8

Гораздо более простой способ - «отключить» взаимодействие с пользователем, включенное в конструкторе интерфейса. «Если вы используете раскадровку»

введите описание изображения здесь

Джефф
источник
6
как другие указывали в аналогичном ответе, это не помогает в этом случае, так как это также отключает сенсорные события в базовом представлении. Другими словами: кнопка под этим видом не может быть нажата, независимо от того, включена или отключена эта настройка.
Auco
6

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

-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
    // Allow buttons to receive press events.  All other views will get ignored
    for( id foundView in self.subviews )
    {
        if( [foundView isKindOfClass:[UIButton class]] )
        {
            UIButton *foundButton = foundView;

            if( foundButton.isEnabled  &&  !foundButton.hidden &&  [foundButton pointInside:[self convertPoint:point toView:foundButton] withEvent:event] )
                return YES;
        }        
    }
    return NO;
}
Тод Каннингем
источник
Нужно проверить, виден ли вид, и видны ли подпредставления.
Ленивый кодер
Идеальное решение! Еще более простой подход заключается в создании UIViewподкласса, PassThroughViewкоторый просто переопределяет, -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; }если вы хотите перенаправить какие-либо сенсорные события под представлениями.
auco
6

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

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

GIF

Как вы можете видеть в GIF, кнопка «Жираф» представляет собой простой прямоугольник, но события касания на прозрачных областях передаются в жёлтый цвет UIButtonснизу.

Ссылка на класс

Segev
источник
1
Хотя это решение может работать, лучше поместить соответствующие части вашего решения в ответ.
Коен.
4

Решение, получившее наибольшее количество голосов, не работало полностью для меня, я полагаю, это потому, что у меня был TabBarController в иерархии (как отмечается в одном из комментариев), он фактически передавал касания некоторым частям пользовательского интерфейса, но это мешало моему способность tableView перехватывать события касания, что, в конце концов, переопределило hitTest в представлении, я хочу игнорировать касания и позволить подвидам этого представления обрабатывать

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *view = [super hitTest:point withEvent:event];
    if (view == self) {
        return nil; //avoid delivering touch events to the container view (self)
    }
    else{
        return view; //the subviews will still receive touch events
    }
}
PakitoV
источник
3

Swift 3

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    for subview in subviews {
        if subview.frame.contains(point) {
            return true
        }
    }
    return false
}
Barath
источник
2

Согласно «Руководству по программированию приложений iPhone»:

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

http://developer.apple.com/iphone/library/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/EventHandling/EventHandling.html

Обновлено : удален пример - перечитайте вопрос ...

Есть ли у вас какие-либо жесты обработки представлений, которые могут обрабатывать касания до того, как кнопка их получит? Кнопка работает, если у вас нет прозрачного вида на нее?

Какие-нибудь примеры кода нерабочего кода?

LK.
источник
Да, я написал в своем вопросе, что нормальные касания работают в нижних представлениях, но кнопки UIB и другие элементы не работают.
Шон Кларк Хесс
1

Насколько я знаю, вы должны сделать это, переопределив метод hitTest : . Я попробовал, но не смог заставить его работать должным образом.

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

Liam
источник
1

Используя советы из других ответов и читая документацию Apple, я создал эту простую библиотеку для решения вашей проблемы:
https://github.com/natrosoft/NATouchThroughView
Она упрощает рисование представлений в Интерфейсном Разработчике, которые должны проходить касания в основной взгляд.

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

Есть демонстрационный проект, и, надеюсь, README хорошо объяснит, что делать. Чтобы обратиться к OP, вы должны изменить прозрачный UIView, который содержит кнопки для класса NATouchThroughView в Интерфейсном Разработчике. Затем найдите чистый UIView, который перекрывает меню, которое вы хотите, чтобы можно было нажать. Измените этот UIView на класс NARootTouchThroughView в Интерфейсном Разработчике. Он может даже быть корневым UIView вашего контроллера представления, если вы намерены передать эти прикосновения к базовому контроллеру представления. Проверьте демонстрационный проект, чтобы увидеть, как он работает. Это действительно довольно просто, безопасно и неинвазивно

n8tr
источник
1

Я создал категорию, чтобы сделать это.

маленький метод извергает и вид золотой.

Заголовок

//UIView+PassthroughParent.h
@interface UIView (PassthroughParent)

- (BOOL) passthroughParent;
- (void) setPassthroughParent:(BOOL) passthroughParent;

@end

Файл реализации

#import "UIView+PassthroughParent.h"

@implementation UIView (PassthroughParent)

+ (void)load{
    Swizz([UIView class], @selector(pointInside:withEvent:), @selector(passthroughPointInside:withEvent:));
}

- (BOOL)passthroughParent{
    NSNumber *passthrough = [self propertyValueForKey:@"passthroughParent"];
    if (passthrough) return passthrough.boolValue;
    return NO;
}
- (void)setPassthroughParent:(BOOL)passthroughParent{
    [self setPropertyValue:[NSNumber numberWithBool:passthroughParent] forKey:@"passthroughParent"];
}

- (BOOL)passthroughPointInside:(CGPoint)point withEvent:(UIEvent *)event{
    // Allow buttons to receive press events.  All other views will get ignored
    if (self.passthroughParent){
        if (self.alpha != 0 && !self.isHidden){
            for( id foundView in self.subviews )
            {
                if ([foundView alpha] != 0 && ![foundView isHidden] && [foundView pointInside:[self convertPoint:point toView:foundView] withEvent:event])
                    return YES;
            }
        }
        return NO;
    }
    else {
        return [self passthroughPointInside:point withEvent:event];// Swizzled
    }
}

@end

Вам нужно будет добавить мои Swizz.h и Swizz.m

Здесь находится

После этого вы просто импортируете UIView + PassthroughParent.h в свой файл {Project} -Prefix.pch, и у каждого представления будет эта возможность.

каждое представление будет принимать точки, но ни одно из пустого пространства не будет.

Я также рекомендую использовать чистый фон.

myView.passthroughParent = YES;
myView.backgroundColor = [UIColor clearColor];

РЕДАКТИРОВАТЬ

Я создал свою собственную сумку собственности, и это не было включено ранее.

Заголовочный файл

// NSObject+PropertyBag.h

#import <Foundation/Foundation.h>

@interface NSObject (PropertyBag)

- (id) propertyValueForKey:(NSString*) key;
- (void) setPropertyValue:(id) value forKey:(NSString*) key;

@end

Файл реализации

// NSObject+PropertyBag.m

#import "NSObject+PropertyBag.h"



@implementation NSObject (PropertyBag)

+ (void) load{
    [self loadPropertyBag];
}

+ (void) loadPropertyBag{
    @autoreleasepool {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Swizz([NSObject class], NSSelectorFromString(@"dealloc"), @selector(propertyBagDealloc));
        });
    }
}

__strong NSMutableDictionary *_propertyBagHolder; // Properties for every class will go in this property bag
- (id) propertyValueForKey:(NSString*) key{
    return [[self propertyBag] valueForKey:key];
}
- (void) setPropertyValue:(id) value forKey:(NSString*) key{
    [[self propertyBag] setValue:value forKey:key];
}
- (NSMutableDictionary*) propertyBag{
    if (_propertyBagHolder == nil) _propertyBagHolder = [[NSMutableDictionary alloc] initWithCapacity:100];
    NSMutableDictionary *propBag = [_propertyBagHolder valueForKey:[[NSString alloc] initWithFormat:@"%p",self]];
    if (propBag == nil){
        propBag = [NSMutableDictionary dictionary];
        [self setPropertyBag:propBag];
    }
    return propBag;
}
- (void) setPropertyBag:(NSDictionary*) propertyBag{
    if (_propertyBagHolder == nil) _propertyBagHolder = [[NSMutableDictionary alloc] initWithCapacity:100];
    [_propertyBagHolder setValue:propertyBag forKey:[[NSString alloc] initWithFormat:@"%p",self]];
}

- (void)propertyBagDealloc{
    [self setPropertyBag:nil];
    [self propertyBagDealloc];//Swizzled
}

@end
Ленивый Кодер
источник
Метод passthroughPointInside хорошо работает для меня (даже без использования passthroughPoint или swizz - просто переименуйте passthroughPointInside в pointInside), большое спасибо.
Бенджамин Пиетт
Что такое propertyValueForKey?
Скотч
1
Хм. Никто не указывал на это раньше. Я построил пользовательский словарь указателя, который содержит свойства в расширении класса. Я посмотрю, смогу ли я найти это, чтобы включить это здесь.
Ленивый Кодер
Методы Swizzling, как известно, являются деликатным хакерским решением для редких случаев и развлечения разработчиков. Это определенно не безопасно в App Store, потому что вполне вероятно, что оно сломает и вылетит из вашего приложения при следующем обновлении ОС. Почему бы просто не переопределить pointInside: withEvent:?
Auco
0

Если вы не можете использовать категорию или подкласс UIView, вы также можете просто переместить кнопку вперед, чтобы она находилась перед прозрачным представлением. Это не всегда возможно в зависимости от вашего приложения, но это сработало для меня. Вы всегда можете вернуть кнопку назад или скрыть ее.

Shaked Sayag
источник
2
Привет, добро пожаловать в стек переполнения. Просто подсказка для вас как нового пользователя: вы можете быть осторожны со своим тоном. Я бы избегал фраз типа «если ты не можешь беспокоиться»; может показаться снисходительным
номинально
Спасибо за внимание. Это было больше с ленивой точки зрения, чем снисходительно, но точка зрения принята.
встряхнул Саяг
0

Попробуй это

class PassthroughToWindowView: UIView {
        override func test(_ point: CGPoint, with event: UIEvent?) -> UIView? {
            var view = super.hitTest(point, with: event)
            if view != self {
                return view
            }

            while !(view is PassthroughWindow) {
                view = view?.superview
            }
            return view
        }
    } 
tBug
источник
0

Попробуйте установить backgroundColorваш transparentViewкак UIColor(white:0.000, alpha:0.020). Затем вы можете получить сенсорные события в touchesBegan/ touchesMovedметодов. Поместите код ниже где-то ваш взгляд:

self.alpha = 1
self.backgroundColor = UIColor(white: 0.0, alpha: 0.02)
self.isMultipleTouchEnabled = true
self.isUserInteractionEnabled = true
Agisight
источник
0

Я использую это вместо переопределения точки метода (внутри: CGPoint, с: UIEvent)

  override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard self.point(inside: point, with: event) else { return nil }
        return self
    }
Вэй
источник