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

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

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

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

EDIT - я все еще хочу иметь возможность взаимодействовать с объектами в верхнем UIView

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

Ответ 1

Вы должны создать подкласс 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:.

Ответ 2

Хотя многие из ответов здесь будут работать, я немного удивлен, увидев, что самый удобный, общий и надежный ответ здесь не приводится. @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, указав, что это представление должно не получать прикосновение. То, как работает цепочка ответчиков, означает, что иерархия представлений выше этой точки будет продолжаться до тех пор, пока не будет найдено представление, которое будет реагировать на касание. Не возвращайте супервизор, так как это не соответствует этому мнению, должен ли его супервизор принимать штрихи или нет!

Это решение:

  • удобный, потому что он не требует ссылок на какие-либо другие представления/подпункты/объекты;
  • generic, потому что он применим к любому представлению, которое действует исключительно как контейнер для сенсорных подзонов, а конфигурация подзонов не влияет на способ его работы (как это происходит, если вы переопределяете 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.

Ответ 3

Есть несколько способов справиться с этим. Моим фаворитом является переопределить 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: и т.д.) для отправки некоторых касаний на другой объект, чем там, где они сначала идут. Например, в вы можете написать примерно следующее:

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

Ответ 4

Вам нужно установить upperView.userInteractionEnabled = NO;, иначе верхний вид перехватит штрихи.

Версия Interface Builder - это флажок внизу панели View Attributes, называемый "User Interaction Enabled". Снимите флажок, и вам должно быть хорошо идти.

Ответ 5

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

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
    return (CGRectContainsPoint(disclosureButton.frame, point));
}

Ответ 6

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

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

GIF

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

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

Ответ 7

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

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

Ответ 8

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

- (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: будет обрабатывать это для вас.

Ответ 9

Устранение свойства userInteraction может помочь. Например:

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

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

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

Ответ 10

Этот подход довольно чист и позволяет прозрачным subviews не реагировать на касания. Просто подкласс 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

Ответ 11

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

Вид сверху:

-(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.
}

Но что идея.

Ответ 12

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

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

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

Ответ 13

Вот версия Swift:

override func pointInside(point: CGPoint, withEvent event: UIEvent?) -> Bool {
    return !CGRectContainsPoint(buttonView.frame, point)
}

Ответ 14

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
}

Ответ 15

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

Каждый 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];

Сообщите мне, если это работает для вашего дела!


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

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

Ответ 16

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

- (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, поэтому вам не нужно объявлять его в своем общедоступном или частном интерфейсе; это один из тех волшебных апи, о которых вы только должны знать. Этот self.superview вызовет запрос в конечном итоге на viewController. В viewController, затем, реализуйте это touchhesBegan, чтобы справиться с прикосновением.

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

Ответ 17

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

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

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

Ответ 18

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

let hitView = super.hitTest(point, with: event)
if hitView == self { return nil }
return hitView

Ответ 19

Полученный от Стюарта превосходный, в основном надежный ответ и полезная реализация Segev, вот пакет 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)
}