Разрешение взаимодействия с UIView под другим UIView

115

Есть ли простой способ разрешить взаимодействие с кнопкой в ​​UIView, который находится под другим UIView - где нет реальных объектов из верхнего UIView поверх кнопки?

Например, на данный момент у меня есть UIView (A) с объектом вверху и объектом внизу экрана и ничего в середине. Он находится поверх другого UIView с кнопками посередине (B). Однако я не могу взаимодействовать с кнопками в середине B.

Я вижу кнопки в B - я установил фон A на clearColor - но кнопки в B, похоже, не получают касаний, несмотря на то, что на самом деле поверх этих кнопок нет объектов из A.

РЕДАКТИРОВАТЬ - я все еще хочу иметь возможность взаимодействовать с объектами в верхнем UIView

Конечно, есть простой способ сделать это?

Делани
источник
2
Практически все это объясняется здесь: developer.apple.com/iphone/library/documentation/iPhone/ ... Но в основном, переопределив hitTest: withEvent :, они даже предоставляют образец кода.
nash
Я написал небольшой класс только для этого. (Добавил пример в ответах). Решение там несколько лучше, чем принятый ответ, потому что вы все равно можете щелкнуть, UIButtonкоторый находится под полупрозрачным, в UIViewто время как непрозрачная часть UIViewвсе еще будет реагировать на события касания.
Сегев

Ответы:

97

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

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // UIView will be "transparent" for touch events if we return NO
    return (point.y < MIDDLE_Y1 || point.y > MIDDLE_Y2);
}

Вы также можете посмотреть метод hitTest: event:.

gyim
источник
19
Что здесь представляет middle_y1 / y2?
Джейсон Ренальдо
Не уверен, что делает это returnутверждение, но return CGRectContainsPoint(eachSubview.frame, point)у меня работает. Чрезвычайно полезный ответ в противном случае
n00neimp0rtant
Бизнес MIDDLE_Y1 / Y2 - всего лишь пример. Эта функция будет «прозрачной» для событий касания в MIDDLE_Y1<=y<=MIDDLE_Y2области.
gyim
41

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

Этот ответ взят из ответа, который я дал на аналогичный вопрос здесь .

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = [super hitTest:point withEvent:event];
    if (hitView == self) return nil;
    return hitView;
}

[super hitTest:point withEvent:event]вернет самое глубокое представление в иерархии этого представления, которое было затронуто. Если hitView == self(то есть, если под точкой касания нет подпредставления), вернитесь nil, указав, что это представление не должно получать касание. То, как работает цепочка респондентов, означает, что иерархия представлений выше этой точки будет продолжаться до тех пор, пока не будет найдено представление, которое будет реагировать на касание. Не возвращайте супервизор, так как это не зависит от того, должен ли супервизор принимать прикосновения или нет!

Это решение:

  • удобно , потому что не требует ссылок на какие-либо другие представления / подпредставления / объекты;
  • общий , потому что он применяется к любому представлению, которое действует исключительно как контейнер для осязаемых подпредставлений, а конфигурация подпредставлений не влияет на способ его работы (как это происходит, если вы переопределяете, pointInside:withEvent:чтобы вернуть конкретную осязаемую область).
  • надежный , кода не так много ... и эту концепцию нетрудно понять.

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

@interface ISView : UIView
@property(nonatomic, assign) BOOL onlyRespondToTouchesInSubviews;
@end

@implementation ISView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
    UIView *hitView = [super hitTest:point withEvent:event];
    if (hitView == self && onlyRespondToTouchesInSubviews) return nil;
    return hitView;
}
@end

Затем сделайте шаг вперед и используйте этот вид везде, где вы можете использовать равнину UIView. Настройка его так просто , как установка onlyRespondToTouchesInSubviewsв YES.

Стюарт
источник
2
Единственно правильный ответ. Чтобы было ясно, если вы хотите игнорировать касания к представлению, но не к каким-либо кнопкам (скажем), которые содержатся в представлении , сделайте, как объясняет Стюарт. (Я обычно называю это «взглядом держателя», потому что он может безвредно «удерживать» некоторые кнопки, но не влияет ни на что «под» держателем.)
Fattie
Некоторые другие решения с использованием точек имеют проблемы, если ваше представление прокручивается, например UITableView или UICollectionView, и вы прокручиваете вверх или вниз. Однако это решение работает независимо от прокрутки.
Pajevic
31

Есть несколько способов справиться с этим. Я предпочитаю переопределить hitTest: withEvent: в представлении, которое является общим супервизором (возможно, косвенно) для конфликтующих представлений (похоже, вы называете их A и B). Например, что-то вроде этого (здесь A и B - указатели UIView, где B - «скрытый», обычно игнорируемый):

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint pointInB = [B convertPoint:point fromView:self];

    if ([B pointInside:pointInB withEvent:event])
        return B;

    return [super hitTest:point withEvent:event];
}

Вы также можете изменить pointInside:withEvent:метод в соответствии с предложением gyim. Это позволяет вам достичь практически того же результата, эффективно «протыкая дыру» в A, по крайней мере, для касаний.

Другой подход - пересылка событий, что означает переопределение touchesBegan:withEvent:и аналогичные методы (например, и touchesMoved:withEvent:т. Д.) Для отправки некоторых штрихов к другому объекту, а не к тому месту, где они были сначала. Например, в A можно написать что-то вроде этого:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if ([self shouldForwardTouches:touches]) {
        [B touchesBegan:touches withEvent:event];
    }
    else {
        // Do whatever A does with touches.
    }
}

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

Есть хороший пост в блоге, объясняющий все это более подробно, а также небольшой рабочий проект xcode для демонстрации идей, доступный здесь:

http://bynomial.com/blog/?p=74

Тайлер
источник
Проблема с этим решением - переопределением hitTest в общем супервизоре - заключается в том, что оно не позволяет нижнему представлению работать полностью правильно, когда нижнее представление является одним из всего набора прокручиваемых представлений (MapView и т. Д.). Насколько я могу судить, переопределение pointInside на виде сверху, как предлагает gyim ниже, работает во всех случаях.
delany
@delany, это неправда; у вас могут быть прокручиваемые представления, расположенные под другими представлениями, и позволить им обоим работать, переопределив hitTest. Вот пример кода: bynomial.com/blogfiles/Temp32.zip
Тайлер
Привет. Не все прокручиваемые представления - только некоторые ... Я попробовал ваш почтовый индекс сверху, изменив UIScrollView на (например) MKMapView, и он не работает. Касания работают - похоже, проблема в прокрутке.
delany
Хорошо, я проверил это и подтвердил, что hitTest не работает так, как вы, возможно, хотели бы с MKMapView. Вы были правы, @delany; хотя он правильно работает с UIScrollView. Интересно, почему MKMapView не работает?
Тайлер
@Tyler Как вы упомянули, «Главное, чтобы встроенные элементы управления, такие как UIButton, всегда игнорировали перенаправленные касания», я хочу знать, как вы это знаете и является ли это официальным документом, объясняющим такое поведение. Я столкнулся с проблемой, что когда UIButton имеет простую подпредставление UIView, он не будет реагировать на события касания в границах подпредставления. И я обнаружил, что событие перенаправляется в UIButton должным образом, как поведение UIView по умолчанию, но я не уверен, что это назначенная функция или просто ошибка. Не могли бы вы показать мне документацию по этому поводу? Большое вам спасибо.
Neal.Marlin 06
29

Ставить надо upperView.userInteractionEnabled = NO;, иначе прикосновения перехватит вид сверху.

Версия для Interface Builder - это флажок в нижней части панели «Атрибуты просмотра», который называется «Взаимодействие с пользователем включено». Снимите флажок, и все будет хорошо.

Бенджамин Кокс
источник
Извините - должен был сказать. Я все еще хочу иметь возможность взаимодействовать с объектами в верхнем UIView.
delany
Но upperView не может получить никаких касаний, включите кнопку в upperView.
imcaptor
2
Это решение работает для меня. Мне вообще не нужен был вид сверху, чтобы реагировать на прикосновения.
TJ
11

Пользовательская реализация pointInside: withEvent: действительно казалась подходящей, но работа с жестко заданными координатами казалась мне странной. В итоге я проверил, находится ли CGPoint внутри кнопки CGRect с помощью функции CGRectContainsPoint ():

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    return (CGRectContainsPoint(disclosureButton.frame, point));
}
samvermette
источник
8

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

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

GIF

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

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

Segev
источник
2
Поскольку ваш код не такой длинный, вы должны включить в свой ответ соответствующие его части на случай, если ваш проект будет перемещен или удален в какой-то момент в будущем.
Гэвин
Спасибо, ваше решение работает в моем случае, когда мне нужна непрозрачная часть моего UIView для ответа на события касания, а прозрачная часть - нет. Brilliant!
Брюс
@ Брюс Рад, что это вам помогло!
Сегев
4

Думаю, я немного опоздал на эту вечеринку, но я добавлю следующее возможное решение:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    UIView *hitView = [super hitTest:point withEvent:event];
    if (hitView != self) return hitView;
    return [self superview];
}

Если вы используете этот код для переопределения стандартной функции hitTest пользовательского UIView, он будет игнорировать ТОЛЬКО само представление. Любые подпредставления этого представления будут возвращать свои обращения в обычном режиме, и любые обращения, которые были бы отправлены в само представление, передаются его суперпредставлению.

-Ash

ясень
источник
1
это мой предпочтительный метод, за исключением того, что я не думаю, что вам следует возвращаться [self superview]. в документации по этому методу состояние «Возвращает самого дальнего потомка получателя в иерархии представлений (включая самого себя), который содержит указанную точку» и «Возвращает ноль, если точка находится полностью вне иерархии представлений получателя». Я думаю, тебе следует вернуться nil. когда вы вернете nil, управление передается супервизору, чтобы он проверил, есть ли у него какие-либо совпадения или нет. поэтому в основном он будет делать то же самое, за исключением того, что возвращение супервизора может что-то сломать в будущем.
jasongregori
Конечно, это, вероятно, было бы разумно (обратите внимание на дату в моем первоначальном ответе - с тех пор было
Эш
4

Просто перебираю принятый ответ и помещаю его здесь для справки. Принятый ответ работает отлично. Вы можете расширить его следующим образом, чтобы позволить подпредставлениям вашего представления получать прикосновение ИЛИ передать его любым представлениям позади нас:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    // If one of our subviews wants it, return YES
    for (UIView *subview in self.subviews) {
        CGPoint pointInSubview = [subview convertPoint:point fromView:self];
        if ([subview pointInside:pointInSubview withEvent:event]) {
            return YES;
        }
    }
    // otherwise return NO, as if userInteractionEnabled were NO
    return NO;
}

Примечание. Вам даже не нужно выполнять рекурсию в дереве вложенного представления, потому что каждый pointInside:withEvent:метод сделает это за вас.

Дэн Розенстарк
источник
3

Может помочь установка отключенного свойства userInteraction. Например:

UIView * topView = [[TOPView alloc] initWithFrame:[self bounds]];
[self addSubview:topView];
[topView setUserInteractionEnabled:NO];

(Примечание: в приведенном выше коде "self" относится к представлению)

Таким образом, вы можете отображать только в topView, но не получите вводимые пользователем данные. Все эти прикосновения пользователя будут проходить через это представление, и нижнее представление будет отвечать за них. Я бы использовал этот topView для отображения прозрачных изображений или их анимации.

Адитья
источник
3

Этот подход довольно чистый и позволяет прозрачным подпредставлениям также не реагировать на прикосновения. Просто создайте подкласс UIViewи добавьте к его реализации следующий метод:

@implementation PassThroughUIView

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    for (UIView *v in self.subviews) {
        CGPoint localPoint = [v convertPoint:point fromView:self];
        if (v.alpha > 0.01 && ![v isHidden] && v.userInteractionEnabled && [v pointInside:localPoint withEvent:event])
            return YES;
    }
    return NO;
}

@end
ProDOS
источник
2

Мое решение здесь:

-(UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    CGPoint pointInView = [self.toolkitController.toolbar convertPoint:point fromView:self];

    if ([self.toolkitController.toolbar pointInside:pointInView withEvent:event]) {
       self.userInteractionEnabled = YES;
    } else {
       self.userInteractionEnabled = NO;
    }

    return [super hitTest:point withEvent:event];
}

Надеюсь это поможет

Джерри
источник
2

Есть кое-что, что вы можете сделать, чтобы перехватить касание в обоих представлениях.

Вид сверху:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
   // Do code in the top view
   [bottomView touchesBegan:touches withEvent:event]; // And pass them on to bottomView
   // You have to implement the code for touchesBegan, touchesEnded, touchesCancelled in top/bottom view.
}

Но это идея.

Александр Кассань
источник
Это, конечно, возможно, но это большая работа (я думаю, вам придется катать свои собственные сенсорные объекты в нижнем слое (например, кнопки)?), И кажется странным, что в этом способ получить поведение, которое казалось бы интуитивным.
delany
Я не уверен, может нам стоит просто попробовать.
Alexandre Cassagne
2

Вот версия Swift:

override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
    return !CGRectContainsPoint(buttonView.frame, point)
}
Esqarrouth
источник
2

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
}
pyRabbit
источник
1

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

Каждый UIView, а это UIWindow, имеет свойство subviews, которое представляет собой массив NSArray, содержащий все подвиды.

Первое вложенное представление, которое вы добавляете к представлению, получит индекс 0, следующее - 1 и так далее. Вы также можете заменить addSubview:на insertSubview: atIndex:или insertSubview:aboveSubview:и такие методы, которые могут определять положение вашего подпредставления в иерархии.

Поэтому проверьте свой код, чтобы увидеть, какое представление вы добавляете первым в свой UIWindow. Это будет 0, другое - 1.
Теперь из одного из ваших вложенных представлений, чтобы перейти к другому, вы должны сделать следующее:

UIView * theOtherView = [[[self superview] subviews] objectAtIndex: 0];
// or using the properties syntax
UIView * theOtherView = [self.superview.subviews objectAtIndex:0];

Сообщите мне, работает ли это в вашем случае!


(под этим маркером мой предыдущий ответ):

Если представлениям необходимо взаимодействовать друг с другом, они должны делать это через контроллер (то есть с использованием популярной модели MVC ).

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

Таким образом, методика состоит в том, чтобы убедиться, что ваши представления регистрируются с помощью контроллера (который может хранить их по имени или как угодно в словаре или массиве). Либо вы можете заставить контроллер отправлять вам сообщение, либо вы можете получить ссылку на представление и напрямую общаться с ним.

Если в вашем представлении нет ссылки на контроллер (что может иметь место), вы можете использовать синглтоны и / или методы класса, чтобы получить ссылку на ваш контроллер.

Нэш
источник
Спасибо за ответ, но я не уверен, что понимаю. У обоих представлений есть контроллер - проблема в том, что с одним представлением поверх другого нижнее представление не собирает события (и не перенаправляет их своему контроллеру), даже если нет объекта, фактически `` блокирующего '' эти события в вид сверху.
delany
У вас есть UIWindow в верхней части наших UIViews? Если вы это сделаете, события должны быть распространены, и вам не нужно будет делать какое-либо «волшебство». Прочтите о [Window and Views] [1] в центре Apple Dev (и обязательно добавьте еще один комментарий, если это вам не поможет!) [1]: developer.apple.com/iphone/library/documentation/ iPhone /…
nash
Да, безусловно - UIWindow наверху иерархии.
delany
Я обновил свой ответ, включив в него код для прохождения иерархии представлений. Дайте мне знать, если вам понадобится другая помощь!
nash
Спасибо за вашу помощь, но у меня достаточно опыта в создании этих интерфейсов, и мой текущий настроен правильно, насколько мне известно. Проблема, по-видимому, заключается в поведении полных представлений по умолчанию, когда они размещаются поверх других.
delany
1

Я думаю, что правильный способ - использовать цепочку представлений, встроенную в иерархию представлений. Для ваших вложенных представлений, которые помещаются в основное представление, не используйте общий UIView, а вместо этого подкласс UIView (или один из его вариантов, например UIImageView), чтобы сделать MYView: UIView (или любой другой супертип, который вы хотите, например UIImageView). В реализации YourView реализуйте метод touchesBegan. Затем этот метод будет вызван при касании этого представления. Все, что вам нужно в этой реализации, - это метод экземпляра:

- (void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event ;
{   // cannot handle this event. pass off to super
    [self.superview touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event]; }

this touchesBegan - это api респондента, поэтому вам не нужно объявлять его в публичном или частном интерфейсе; это одно из тех волшебных API, о которых вам просто нужно знать. Этот self.superview в конечном итоге передаст запрос к viewController. Затем в viewController реализуйте touchBegan для обработки прикосновения.

Обратите внимание, что место касания (CGPoint) автоматически настраивается относительно окружающего представления для вас, поскольку оно перемещается вверх по цепочке иерархии представлений.

Папа Смурф
источник
1

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

 for(UIGestureRecognizer *recognizer in topView.gestureRecognizers)
 {
     recognizer.delegate=self;
     [bottomView addGestureRecognizer:recognizer];   
 }
 topView.abView.userInteractionEnabled=NO; 

и реализуя UIGestureRecognizerDelegate:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

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

stringCode
источник
1

Внедрение Swift 4 для решения на основе HitTest

let hitView = super.hitTest(point, with: event)
if hitView == self { return nil }
return hitView
BiscottiGelato
источник
0

Полученный из превосходного и в основном надежного ответа Стюарта и полезной реализации Сегева, вот пакет Swift 4, который вы можете добавить в любой проект:

extension UIColor {
    static func colorOfPoint(point:CGPoint, in view: UIView) -> UIColor {

        var pixel: [CUnsignedChar] = [0, 0, 0, 0]

        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)

        let context = CGContext(data: &pixel, width: 1, height: 1, bitsPerComponent: 8, bytesPerRow: 4, space: colorSpace, bitmapInfo: bitmapInfo.rawValue)

        context!.translateBy(x: -point.x, y: -point.y)

        view.layer.render(in: context!)

        let red: CGFloat   = CGFloat(pixel[0]) / 255.0
        let green: CGFloat = CGFloat(pixel[1]) / 255.0
        let blue: CGFloat  = CGFloat(pixel[2]) / 255.0
        let alpha: CGFloat = CGFloat(pixel[3]) / 255.0

        let color = UIColor(red:red, green: green, blue:blue, alpha:alpha)

        return color
    }
}

А затем с помощью hitTest:

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard UIColor.colorOfPoint(point: point, in: self).cgColor.alpha > 0 else { return nil }
    return super.hitTest(point, with: event)
}
brandonscript
источник