hitTest : withEvent를 사용하여 수퍼 뷰의 프레임 외부에있는 하위 뷰에 대한 터치 캡처 :


94

내 문제 :EditView 기본적으로 전체 애플리케이션 프레임을 차지하는 수퍼 뷰 와 MenuView하단 ~ 20 % 만 차지하는 MenuView하위 뷰가 ButtonView있으며 실제로 MenuView의 경계 밖에있는 자체 하위 뷰 를 포함합니다 (예 :) ButtonView.frame.origin.y = -100.

(참고 : 의 뷰 계층 구조의 EditView일부가 MenuView아니지만 답변에 영향을 미칠 수있는 다른 하위 뷰가 있습니다 .)

이미이 문제를 알고있을 것입니다 ButtonView. 범위 내에 있을 때 MenuView(또는 더 구체적으로 내 터치가 MenuView의 범위 내에있을 때 ) ButtonView는 터치 이벤트에 응답합니다. 내 터치가 MenuView의 범위를 벗어 났지만 여전히 ButtonView의 범위 내에 있으면에서 터치 이벤트를받지 않습니다 ButtonView.

예:

  • (E)는 EditView모든보기의 상위입니다.
  • (M)은 MenuViewEditView의 하위보기입니다.
  • (B)는 ButtonViewMenuView의 하위보기입니다.

도표:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

(B)가 (M)의 프레임 밖에 있기 때문에 (B) 영역의 탭은 (M)으로 전송되지 않습니다. 사실, (M)은이 경우 터치를 분석하지 않으며 터치는 계층 구조의 다음 개체.

목표 : 재정의 hitTest:withEvent:가이 문제를 해결할 수 있다고 생각 하지만 정확히 어떻게해야하는지 모르겠습니다. 제 경우에는 (내 '마스터'수퍼 뷰) hitTest:withEvent:에서 재정의 해야 EditView합니까? 아니면 MenuView터치를받지 않는 버튼의 직접 수퍼 뷰 인 에서 재정의해야 합니까? 아니면 이것에 대해 잘못 생각하고 있습니까?

긴 설명이 필요하다면 좋은 온라인 리소스가 도움이 될 것입니다. Apple의 UIView 문서를 제외하고는 분명하지 않습니다.

감사!

답변:


145

나는 수락 된 답변의 코드를보다 일반적으로 수정했습니다.보기가 하위보기를 경계로 자르고 숨길 수 있으며 더 중요한 경우를 처리합니다. 하위보기가 복잡한보기 계층 인 경우 올바른 하위보기가 반환됩니다.

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {

    if (self.clipsToBounds) {
        return nil;
    }

    if (self.hidden) {
        return nil;
    }

    if (self.alpha == 0) {
        return nil;
    }

    for (UIView *subview in self.subviews.reverseObjectEnumerator) {
        CGPoint subPoint = [subview convertPoint:point fromView:self];
        UIView *result = [subview hitTest:subPoint withEvent:event];

        if (result) {
            return result;
        }
    }

    return nil;
}

SWIFT 3

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {

    if clipsToBounds || isHidden || alpha == 0 {
        return nil
    }

    for subview in subviews.reversed() {
        let subPoint = subview.convert(point, from: self)
        if let result = subview.hitTest(subPoint, with: event) {
            return result
        }
    }

    return nil
}

더 복잡한 사용 사례에이 솔루션을 사용하려는 모든 사람에게 도움이되기를 바랍니다.


이것은 좋아 보이고 그것을 해주셔서 감사합니다. 그러나 한 가지 질문이있었습니다. 왜 return [super hitTest : point withEvent : event]; ? 터치를 트리거하는 하위 뷰가 없으면 nil을 반환하지 않겠습니까? Apple은 하위보기에 터치가 포함되어 있지 않으면 hitTest가 nil을 반환한다고 말합니다.
Ser Pounce 2013 년

흠 ... 맞아요. 또한 올바른 동작을 위해서는 객체를 역순으로 반복해야합니다 (마지막 항목이 시각적으로 최상위 항목이기 때문에). 일치하도록 수정 된 코드.
Noam 2013 년

3
방금 솔루션을 사용하여 UIButton이 터치를 캡처하도록 만들었으며 UICollectionView 내부의 UICollectionViewCell 내부에있는 UIView 내부에 있습니다. 이 세 클래스에서 hitTest : withEvent :를 재정의하기 위해 UICollectionView와 UICollectionViewCell을 하위 클래스로 만들어야했습니다. 그리고 그것은 매력처럼 작동합니다! 감사 !!
Daniel García

3
원하는 용도에 따라 [super hitTest : point withEvent : event] 또는 nil을 반환해야합니다. 자신을 되 돌리면 모든 것을 받게됩니다.
Noam

1
다음은 동일한 기술에 대한 Apple의 Q & A 기술 문서입니다. developer.apple.com/library/ios/qa/qa2013/qa1812.html
James Kuang

33

좋아, 나는 약간의 파고와 테스트를 hitTest:withEvent했다. 적어도 높은 수준에서 작동 하는 방법 은 다음과 같다. 이 시나리오 이미지 :

  • (E)는 모든 뷰의 부모 인 EditView입니다 .
  • (M)은 EditView의 하위보기 인 MenuView 입니다.
  • (B)는 MenuView의 하위보기 인 ButtonView 입니다.

도표:

+------------------------------+
|E                             |
|                              |
|                              |
|                              |
|                              |
|+-----+                       |
||B    |                       |
|+-----+                       |
|+----------------------------+|
||M                           ||
||                            ||
|+----------------------------+|
+------------------------------+

(B)가 (M)의 프레임 밖에 있기 때문에 (B) 영역의 탭은 (M)으로 전송되지 않습니다. 사실, (M)은이 경우 터치를 분석하지 않으며 터치는 계층 구조의 다음 개체.

그러나 hitTest:withEvent:(M)에서 구현 하면 응용 프로그램의 아무 곳이나 탭이 (M)로 전송됩니다 (또는 최소한 알고 있음). 이 경우 터치를 처리하고 터치를 받아야하는 객체를 반환하는 코드를 작성할 수 있습니다.

더 구체적으로 말하면 목표 hitTest:withEvent:는 히트를 받아야하는 객체를 반환하는 것입니다. 따라서 (M)에서 다음과 같은 코드를 작성할 수 있습니다.

// need this to capture button taps since they are outside of self.frame
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{   
    for (UIView *subview in self.subviews) {
        if (CGRectContainsPoint(subview.frame, point)) {
            return subview;
        }
    }

    // use this to pass the 'touch' onward in case no subviews trigger the touch
    return [super hitTest:point withEvent:event];
}

나는 여전히이 방법 과이 문제에 대해 매우 생소하므로 코드를 작성하는 더 효율적이거나 올바른 방법이 있으면 의견을 말하십시오.

나중에이 질문에 답하는 다른 사람에게 도움이되기를 바랍니다. :)


감사합니다. 실제로 히트를 받아야하는 하위 뷰를 결정하기 위해 더 많은 논리를 수행해야했지만 이것은 저에게 효과적이었습니다. 나는 당신의 예제 btw에서 불필요한 휴식을 제거했습니다.
Daniel Saidi 2014

서브 뷰 프레임이 상위 뷰의 프레임 이상일 때 클릭 이벤트가 응답하지 않았습니다! 솔루션이이 문제를 해결했습니다. 고마워요 :-)
byJeevan

@toblerpwn (재미있는 별명 :))이 훌륭한 이전 답변을 편집하여 이것을 추가 해야하는 클래스 를 매우 명확하게 만들어야합니다 (큰 대문자 사용) . 건배!
Fattie

26

Swift 5에서

override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
    guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
    for member in subviews.reversed() {
        let subPoint = member.convert(point, from: self)
        guard let result = member.hitTest(subPoint, with: event) else { continue }
        return result
    }
    return nil
}

이것은 "오작동"보기 의 직접 수퍼 뷰에 재정의를 적용하는 경우에만 작동합니다
Hudi Ilfeld

2

내가 할 일은 ButtonView와 MenuView를 모두 프레임에 완전히 맞는 컨테이너에 배치하여 뷰 계층 구조에서 동일한 수준에 존재하도록하는 것입니다. 이렇게하면 잘린 항목의 대화 형 영역이 수퍼 뷰의 경계로 인해 무시되지 않습니다.


이 해결 방법에 대해서도 생각했습니다. 배치 로직을 복제 (또는 심각한 코드를 리팩토링해야합니다!)해야하지만 결국에는 이것이 최선의 선택 일 수 있습니다.
toblerpwn

1

상위 뷰 내에 다른 하위 뷰가 많이있는 경우 위의 솔루션을 사용하면 대부분의 다른 대화 형 뷰가 작동하지 않을 것입니다.이 경우 다음과 같은 것을 사용할 수 있습니다 (Swift 3.2에서).

class BoundingSubviewsViewExtension: UIView {

    @IBOutlet var targetView: UIView!

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        // Convert the point to the target view's coordinate system.
        // The target view isn't necessarily the immediate subview
        let pointForTargetView: CGPoint? = targetView?.convert(point, from: self)
        if (targetView?.bounds.contains(pointForTargetView!))! {
            // The target view may have its view hierarchy,
            // so call its hitTest method to return the right hit-test view
            return targetView?.hitTest(pointForTargetView ?? CGPoint.zero, with: event)
        }
        return super.hitTest(point, with: event)
    }
}

0

누구든지 그것을 필요로한다면 여기에 신속한 대안이 있습니다.

override func hitTest(point: CGPoint, withEvent event: UIEvent?) -> UIView? {
    if !self.clipsToBounds && !self.hidden && self.alpha > 0 {
        for subview in self.subviews.reverse() {
            let subPoint = subview.convertPoint(point, fromView:self);

            if let result = subview.hitTest(subPoint, withEvent:event) {
                return result;
            }
        }
    }

    return nil
}

0

보기 계층 구조에 코드 줄 아래에 배치하십시오.

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

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event
{
    CGRect rect = self.bounds;
    BOOL isInside = CGRectContainsPoint(rect, point);
    if(!isInside)
    {
        for (UIView *view in self.subviews)
        {
            isInside = CGRectContainsPoint(view.frame, point);
            if(isInside)
                break;
        }
    }
    return isInside;
}

더 자세한 설명을 위해 내 블로그 "goaheadwithiphonetech"에서 "사용자 정의 콜 아웃 : 버튼을 클릭 할 수없는 문제"에 대해 설명했습니다.

도움이 되었기를 바랍니다 ... !!!


귀하의 블로그가 삭제되었으므로 어디에서 설명을 찾을 수 있습니까?
ishahak
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.