Дозволити взаємодію з UIView під іншим UIView


115

Чи є простий спосіб дозволити взаємодію з кнопкою в UIView, що лежить під іншим UIView - там, де немає фактичних об'єктів зверху UIView вгорі кнопки?

Наприклад, на даний момент у мене є UIView (A) з об'єктом вгорі і об'єктом внизу екрана і нічого в середині. Це розташовується поверх іншого UIView, який має кнопки посередині (B). Однак мені здається, що я не можу взаємодіяти з кнопками в середині B.

Я бачу кнопки в B - я встановив фон A на clearColor - але кнопки в B, схоже, не отримують дотиків, незважаючи на те, що поверх цих кнопок насправді немає об’єктів від A.

EDIT - Я все ще хочу мати можливість взаємодіяти з об’єктами у верхньому UIView

Напевно, існує простий спосіб зробити це?


2
Тут майже все пояснено: developer.apple.com/iphone/library/documentation/iPhone/… Але, в основному, переосмислюйте hitTest: withEvent:, вони навіть надають зразок коду.
наш

Я написав невеликий клас саме для цього. (Додано приклад у відповідях). Рішення є дещо кращим, ніж прийнята відповідь, оскільки ви все одно можете натиснути кнопку, 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:.


19
Що тут представлено mid_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 тощо). Переважна точкаInside на вид зверху, як це запропонував gyim внизу, працює у всіх випадках, наскільки я можу сказати.
затримка

@delany, Це неправда; ви можете прокручувати подання, що знаходяться під іншими видами, і дозволити їм обом працювати, переосмисливши HitTest. Ось приклад коду: bynomial.com/blogfiles/Temp32.zip
Тайлер

Гей. Не всі прокручувані представлення - лише деякі ... Я спробував ваш поштовий індекс зверху, змінивши UIScrollView на (наприклад) MKMapView, і він не працює. Робота з кранами - це, мабуть, проблема.
затримка

Гаразд, я перевірив це і підтвердив, що HitTest не працює так, як вам потрібно з MKMapView. Ви мали рацію з цього приводу, @delany; хоча це працює коректно з UIScrollView. Цікаво, чому MKMapView виходить з ладу?
Тайлер

@Tyler Як ви вже згадували, що "Головне, щоб вбудовані елементи управління, такі як UIButton, завжди ігнорували переслані штрихи", я хочу знати, як ви це знаєте і чи є офіційним документом пояснення такої поведінки. Я зіткнувся з проблемою, що коли UIButton має звичайний підпогляд UIView, він не реагуватиме на торкання подій у межах субперегляду. І я виявив, що подія передається UIButton належним чином як поведінка за замовчуванням UIView, але я не впевнений, що це призначена функція або просто помилка. Покажіть, будь ласка, документацію щодо цього? Дякую тобі щиро.
Ніл.Марлін

29

Вам доведеться встановити upperView.userInteractionEnabled = NO;, інакше верхній вигляд перехопить штрихи.

Версія для створення інтерфейсу для цього - це прапорець у нижній частині панелі Перегляд атрибутів під назвою "Взаємодія користувача ввімкнена". Зніміть прапорець, і вам слід добре піти.


Вибачте - мав би сказати. Я все ще хочу мати можливість взаємодіяти з об’єктами у верхньому UIView.
затримка

Але верхній перегляд не може отримати жодного дотику, включіть кнопку в верхній перегляд.
імператор

2
Це рішення працює для мене. Мені не потрібен був вид зверху, щоб реагувати на дотики.
TJ

11

Спеціальна реалізація pointInside: withEvent: справді здавалася дорогою, але робота з жорстко закодованими координатами видалася мені дивною. Тож я закінчив перевірити, чи знаходиться CGPoint всередині кнопки CGRect за допомогою функції CGRectContainsPoint ():

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

8

Нещодавно я написав клас, який допоможе мені просто в цьому. Використовуючи його як нестандартний клас для UIButtonабо UIViewпередаватиме сенсорні події, виконані на прозорому пікселі.

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

GIF

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

Посилання на клас


2
Оскільки ваш код не такий довгий, вам слід включити відповідні фрагменти у свою відповідь, якщо ваш проект буде переміщений або видалений в якийсь момент майбутнього.
Гавін

Дякую, ваше рішення працює для мого випадку, коли мені потрібна непрозора частина мого UIView, щоб реагувати на події, що торкаються, тоді як прозора частина - ні. Блискуче!
Брюс

@Bruce Радий, що це допомогло тобі!
Сегев

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, вона буде ігнорувати ТОЛЬКО сам вид. Будь-які підпогляди цього перегляду нормально повернуть свої звернення, а будь-які звернення, що перейшли до самого перегляду, передаються до його перегляду.

-Зола


1
це мій кращий метод, за винятком того, я не думаю, що вам слід повертатися [self superview]. документація щодо цього способу "Повертає найдальшого нащадка приймача в ієрархії перегляду (включаючи себе), що містить задану точку" та "Повертає нуль, якщо точка повністю лежить поза ієрархією подання огляду". я думаю, ти повинен повертатися 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

Налаштування властивості користувачевої взаємодії вимкнено може допомогти. Наприклад:

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

(Примітка. У наведеному вище коді "Я" позначає вигляд)

Таким чином, ви можете відображатись лише у 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

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

Але це ідея.


Це, безумовно, можливо - але це велика робота (вам доведеться згорнути власні чутливі до дотику об'єкти в нижній шар (наприклад, кнопки), я думаю?), І здається дивним, що вам доведеться прокрутити свій власний в цьому спосіб отримати поведінку, яка, здавалося б, інтуїтивно зрозуміла.
затримка

Я не впевнений, можливо, нам варто просто спробувати.
Олександр Кассань

2

Ось версія Swift:

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

2

Швидкий 3

override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
    for subview in subviews {
        if subview.frame.contains(point) {
            return true
        }
    }
    return false
}

1

Я ніколи не будував повний інтерфейс користувача за допомогою інструментарію інтерфейсу користувача, тому не маю великого досвіду роботи з ним. Ось, що я думаю, що має працювати.

Кожен UIView, а це і вікно UIW, має властивість subviews- це NSArray, що містить усі підпогляди.

Перший підвід, який ви додасте до представлення, отримає індекс 0, а наступний індекс 1 тощо. Крім того, можна замінити addSubview:з insertSubview: atIndex:або insertSubview:aboveSubview:і такими методами , які можуть визначити положення вашого підвиду в ієрархії.

Тому перевірте свій код, щоб побачити, який вид ви додасте спочатку до свого вікна UIW. Це буде 0, інше - 1.
Тепер з одного з ваших підглядів, щоб дійти до іншого, ви зробите наступне:

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

Дайте мені знати, чи це працює для вашої справи!


(нижче цього маркера - моя попередня відповідь):

Якщо поглядам потрібно спілкуватися один з одним, вони повинні робити це через контролер (тобто, використовуючи популярну модель MVC ).

Створюючи новий вид, ви можете переконатися, що він реєструється у контролері.

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

Якщо у вашому представництві немає зворотного зв’язку контролера (що може бути), ви можете скористатися одинарними класами та / або методами класу, щоб отримати посилання на свій контролер.


Дякую за вашу відповідь - але я не впевнений, що розумію. Обидва погляди мають контролер - проблема полягає в тому, що, коли один погляд зверху іншого, подання знизу не збирає події (і пересилає їх до свого контролера), навіть через те, що жоден об'єкт фактично не блокує ці події в вид зверху
затримка

Чи є у вас вікно UI у верхній частині наших UIView? Якщо ви це робите, події слід поширювати, і вам не потрібно робити ніяких "магічних". Прочитайте про [Вікно та перегляди] [1] у центрі Apple Dev (і, будь-ласка, додайте ще один коментар, якщо це вам не допоможе!) [1]: developer.apple.com/iphone/library/documentation/ iPhone /…
наш

Так, абсолютно - вікно UI у верхній частині ієрархії.
затримка

Я оновив свою відповідь, щоб включити код для проходження ієрархії поглядів. Повідомте мене, якщо вам потрібна інша допомога!
наш

Дякую за вашу допомогу, але я досить досвід роботи зі створенням цих інтерфейсів, і мій поточний налаштований правильно, наскільки я знаю. Схоже, проблема полягає у поведінці повних поглядів за замовчуванням, коли вони розміщуються поверх інших.
затримка

1

Я думаю, що правильним способом є використання ланцюга перегляду, вбудованого в ієрархію перегляду. Для ваших субпрезентацій, які висуваються на основний вигляд, не використовуйте загальний UIView, а замість підкласу UIView (або одного з його варіантів, наприклад UIImageView), щоб зробити MYView: UIView (або будь-який супертип, який ви хочете, наприклад, UIImageView). У реалізації для YourView реалізуйте метод touchBegan. Після цього буде застосовано цей метод, коли торкнеться цього виду. Все, що вам потрібно мати в цій реалізації, - це метод примірника:

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

це touchchesBegan - це api-відповідь, тому вам не потрібно заявляти про це у своєму публічному чи приватному інтерфейсі; це одна з тих чарівних апі, про які ти просто повинен знати. Цей самоперевірка призведе до запиту в кінцевому підсумку до viewController. У режимі перегляду Контролер, тоді, застосуйте цей штрихпочаток, щоб обробити дотик.

Зауважте, що розташування дотиків (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;
}

Знизу був навігаційний контролер з кількістю сег, і я мав на зразок двері, яка могла закритись жестом пан. Вся річ була вбудована в ще одну ВК. Працював як шарм. Сподіваюся, це допомагає.


1

Швидке впровадження для HitTest-рішення

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

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)
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.