Làm cách nào tôi có thể nhấp vào nút phía sau UIView trong suốt?


176

Giả sử chúng ta có một bộ điều khiển xem với một chế độ xem phụ. subview chiếm trung tâm của màn hình với lề 100 px ở tất cả các phía. Sau đó chúng tôi thêm một loạt các công cụ nhỏ để nhấp vào bên trong đó. Chúng tôi chỉ sử dụng chế độ xem phụ để tận dụng khung mới (x = 0, y = 0 bên trong chế độ xem phụ thực sự là 100.100 trong chế độ xem chính).

Sau đó, hãy tưởng tượng rằng chúng ta có một cái gì đó đằng sau cuộc phỏng vấn, như một menu. Tôi muốn người dùng có thể chọn bất kỳ "công cụ nhỏ" nào trong chế độ xem phụ, nhưng nếu không có gì ở đó, tôi muốn chạm để vượt qua nó (vì dù sao thì nền vẫn rõ ràng) cho các nút phía sau nó.

Tôi có thể làm cái này như thế nào? Có vẻ như TouchBegan đi qua, nhưng các nút không hoạt động.


1
Tôi nghĩ rằng UIViews trong suốt (alpha 0) không được phép phản ứng với các sự kiện chạm?
Evadne Wu

1
Tôi đã viết một lớp học nhỏ chỉ vì điều đó. (Đã thêm một ví dụ trong các câu trả lời). Giải pháp ở đó có phần tốt hơn câu trả lời được chấp nhận vì bạn vẫn có thể nhấp vào câu trả lời UIButtondưới dạng bán trong suốt UIViewtrong khi phần không trong suốt của phần UIViewvẫn sẽ phản hồi với các sự kiện chạm.
Segev

Câu trả lời:


314

Tạo chế độ xem tùy chỉnh cho vùng chứa của bạn và ghi đè thông báo pointInside: trả về sai khi điểm không nằm trong chế độ xem con đủ điều kiện, như thế này:

Nhanh:

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

Mục tiêu 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

Sử dụng chế độ xem này làm vùng chứa sẽ cho phép bất kỳ đứa trẻ nào của nó nhận được các lần chạm nhưng bản thân chế độ xem sẽ trong suốt đối với các sự kiện.


4
Hấp dẫn. Tôi cần phải đi sâu vào chuỗi phản hồi nhiều hơn.
Sean Clark Hess

1
bạn cũng cần kiểm tra xem chế độ xem có hiển thị hay không, sau đó bạn cũng cần kiểm tra xem các bản xem trước có hiển thị hay không trước khi kiểm tra chúng.
Bộ giải mã lười biếng

2
Thật không may, thủ thuật này không hoạt động đối với tabBarControll mà chế độ xem không thể thay đổi. Bất cứ ai cũng có một ý tưởng để làm cho quan điểm này minh bạch cho các sự kiện quá?
cyrilchampier

1
Bạn cũng nên kiểm tra alpha của chế độ xem. Thường thì tôi sẽ ẩn chế độ xem bằng cách đặt alpha thành 0. Một khung nhìn với zero alpha sẽ hoạt động như một khung nhìn ẩn.
James Andrew

2
Tôi đã thực hiện một phiên bản nhanh chóng: có lẽ bạn có thể đưa nó vào câu trả lời cho khả năng hiển thị gist.github.com/eyeballz/17945454447c7ae766cb
eyeballz

107

Tôi cũng dùng

myView.userInteractionEnabled = NO;

Không cần phải phân lớp. Hoạt động tốt.


76
Điều này cũng sẽ vô hiệu hóa tương tác người dùng cho bất kỳ
cuộc phỏng vấn

Như @pixelfreak đã đề cập, đây không phải là giải pháp lý tưởng cho mọi trường hợp. Các trường hợp tương tác trên các lượt xem của chế độ xem đó vẫn được yêu cầu yêu cầu cờ đó vẫn được bật.
Augusto Carmo

20

Từ Apple:

Chuyển tiếp sự kiện là một kỹ thuật được sử dụng bởi một số ứng dụng. Bạn chuyển tiếp các sự kiện chạm bằng cách gọi các phương thức xử lý sự kiện của một đối tượng phản hồi khác. Mặc dù đây có thể là một kỹ thuật hiệu quả, bạn nên thận trọng khi sử dụng nó. Các lớp của khung UIKit không được thiết kế để nhận các liên lạc không bị ràng buộc với chúng .... Nếu bạn muốn chuyển tiếp chạm có điều kiện tới các phản hồi khác trong ứng dụng của bạn, tất cả các phản hồi này phải là các thể hiện của các lớp con UIView của riêng bạn.

Táo thực hành tốt nhất :

Không rõ ràng gửi các sự kiện lên chuỗi phản hồi (thông qua nextResponder); thay vào đó, hãy gọi triển khai siêu lớp và để UIKit xử lý truyền tải chuỗi phản hồi.

thay vào đó bạn có thể ghi đè:

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

trong lớp con UIView của bạn và trả về KHÔNG nếu bạn muốn liên lạc đó được gửi lên chuỗi phản hồi (IE để xem phía sau chế độ xem của bạn mà không có gì trong đó).


1
Thật tuyệt vời khi có một liên kết đến các tài liệu bạn trích dẫn, cùng với các trích dẫn.
morgancodes

1
Tôi đã thêm các liên kết đến các địa điểm có liên quan trong tài liệu cho bạn
jackslash

1
~ ~ Bạn có thể chỉ ra cách ghi đè đó sẽ trông như thế nào không? ~ ~ Tìm thấy câu trả lời trong một câu hỏi khác về việc nó sẽ trông như thế nào; nó thực sự chỉ trở lại NO;
kontur

Đây có lẽ nên là câu trả lời được chấp nhận. Đó là cách đơn giản nhất và cách được đề xuất.
Ecuador

8

Một cách đơn giản hơn nhiều là "Bỏ kiểm tra" Tương tác người dùng được bật trong trình tạo giao diện. "Nếu bạn đang sử dụng bảng phân cảnh"

nhập mô tả hình ảnh ở đây


6
như những người khác đã chỉ ra trong một câu trả lời tương tự, điều này không giúp ích gì trong trường hợp này, vì nó cũng vô hiệu hóa các sự kiện chạm trong chế độ xem bên dưới. Nói cách khác: nút bên dưới chế độ xem đó không thể được gõ, bất kể bạn có bật hoặc tắt cài đặt này hay không.
auco

6

Dựa trên những gì John đăng, đây là một ví dụ cho phép các sự kiện chạm đi qua tất cả các lượt xem của chế độ xem ngoại trừ các nút:

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

Cần kiểm tra xem chế độ xem có thể nhìn thấy hay không và nếu các cuộc phỏng vấn được hiển thị.
Bộ giải mã lười biếng

Giải pháp hoàn hảo! Một cách tiếp cận thậm chí đơn giản hơn sẽ là tạo một UIViewlớp con PassThroughViewchỉ ghi đè -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; }nếu bạn muốn chuyển tiếp bất kỳ sự kiện chạm nào đến bên dưới chế độ xem.
auco

6

Gần đây tôi đã viết một lớp sẽ giúp tôi chỉ với điều đó. Sử dụng nó như một lớp tùy chỉnh cho một UIButtonhoặc UIViewsẽ vượt qua các sự kiện chạm được thực hiện trên một pixel trong suốt.

Giải pháp này có phần tốt hơn câu trả lời được chấp nhận vì bạn vẫn có thể nhấp vào câu trả lời UIButtondưới dạng bán trong suốt UIViewtrong khi phần không trong suốt của di UIViewchúc vẫn phản hồi các sự kiện chạm.

QUÀ TẶNG

Như bạn có thể thấy trong GIF, nút Gi hươu cao cổ là một hình chữ nhật đơn giản nhưng các sự kiện chạm vào các khu vực trong suốt được chuyển sang màu vàng UIButtonbên dưới.

Liên kết với lớp


1
Trong khi giải pháp này có thể hoạt động, tốt hơn là đặt các phần có liên quan của giải pháp của bạn trong câu trả lời.
Koen.

4

Giải pháp được bình chọn hàng đầu không hoàn toàn phù hợp với tôi, tôi đoán đó là do tôi đã có TabBarControll vào hệ thống phân cấp (như một trong những ý kiến ​​chỉ ra) trên thực tế, nó đã chạm vào một số phần của giao diện người dùng nhưng nó đã gây rối với tôi Khả năng chặn các sự kiện chạm của bảngView, điều cuối cùng đã làm là ghi đè hitTest trong chế độ xem Tôi muốn bỏ qua các lần chạm và để các cuộc phỏng vấn của chế độ xem đó xử lý chúng

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

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
}

2

Theo 'Hướng dẫn lập trình ứng dụng iPhone':

Tắt cung cấp các sự kiện liên lạc. Theo mặc định, chế độ xem nhận các sự kiện chạm, nhưng bạn có thể đặt thuộc tính userInteractionEnables của nó thành KHÔNG để tắt phân phối các sự kiện. Một khung nhìn cũng không nhận được các sự kiện nếu nó bị ẩn hoặc nếu nó trong suốt .

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

Đã cập nhật : Đã xóa ví dụ - đọc lại câu hỏi ...

Bạn có bất kỳ xử lý cử chỉ nào trên các chế độ xem có thể xử lý các vòi trước khi nút đó không? Nút có hoạt động khi bạn không có chế độ xem trong suốt không?

Bất kỳ mẫu mã của mã không làm việc?


Có, tôi đã viết trong câu hỏi của mình rằng các chạm thông thường hoạt động ở các chế độ xem bên dưới, nhưng UIButtons và các yếu tố khác không hoạt động.
Sean Clark Hess

1

Theo tôi biết, bạn phải có khả năng thực hiện điều này bằng cách ghi đè phương thức hitTest : . Tôi đã thử nó nhưng không thể làm cho nó hoạt động đúng.

Cuối cùng, tôi đã tạo ra một loạt các chế độ xem trong suốt xung quanh đối tượng có thể chạm để chúng không che nó. Bit của một hack cho vấn đề của tôi điều này làm việc tốt.


1

Nhận lời khuyên từ các câu trả lời khác và đọc tài liệu của Apple, tôi đã tạo thư viện đơn giản này để giải quyết vấn đề của bạn:
https://github.com/natrosoft/NATouchThroughView
Giúp bạn dễ dàng vẽ các chế độ xem trong Trình tạo giao diện có thể vượt qua một cái nhìn cơ bản.

Tôi nghĩ rằng việc thay đổi phương pháp là quá mức cần thiết và rất nguy hiểm khi thực hiện mã sản xuất vì bạn đang trực tiếp làm hỏng việc triển khai cơ sở của Apple và thực hiện một thay đổi trên toàn ứng dụng có thể gây ra hậu quả không lường trước.

Có một dự án demo và hy vọng README làm tốt công việc giải thích những việc cần làm. Để giải quyết OP, bạn sẽ thay đổi UIView rõ ràng có chứa các nút thành lớp NATouchThroughView trong Trình tạo giao diện. Sau đó tìm UIView rõ ràng che phủ menu mà bạn muốn có thể nhấn. Thay đổi UIView đó thành lớp NARootTouchThroughView trong Trình tạo giao diện. Nó thậm chí có thể là UIView gốc của trình điều khiển chế độ xem của bạn nếu bạn dự định những lần chạm đó chuyển qua trình điều khiển chế độ xem bên dưới. Kiểm tra dự án demo để xem nó hoạt động như thế nào. Nó thực sự khá đơn giản, an toàn và không xâm lấn


1

Tôi đã tạo ra một thể loại để làm điều này.

một chút phương pháp swizzling và xem là vàng.

Tiêu đề

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

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

@end

Các tập tin thực hiện

#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

Bạn sẽ cần thêm Swizz.h và Swizz.m của tôi

nằm ở đây

Sau đó, bạn chỉ cần nhập UIView + PassthroughParent.h trong tệp {Project} -Prefix.pch của bạn và mọi chế độ xem sẽ có khả năng này.

mọi chế độ xem sẽ lấy điểm, nhưng không có khoảng trống nào.

Tôi cũng khuyên bạn nên sử dụng một nền tảng rõ ràng.

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

BIÊN TẬP

Tôi đã tạo túi tài sản của riêng mình và điều đó không được bao gồm trước đây.

Tập tin tiêu đề

// NSObject+PropertyBag.h

#import <Foundation/Foundation.h>

@interface NSObject (PropertyBag)

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

@end

Tập tin thực hiện

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

Phương pháp passthroughPointInside đang hoạt động tốt đối với tôi (ngay cả khi không sử dụng bất kỳ công cụ passthroughparent hoặc swizz nào - chỉ cần đổi tên passthroughPointInside thành pointInside), cảm ơn rất nhiều.
Benjamin Piette

Tài sảnValueForKey là gì?
Skotch

1
Hừm. Không ai chỉ ra điều đó trước đây. Tôi đã xây dựng một từ điển con trỏ tùy chỉnh chứa các thuộc tính trong một phần mở rộng lớp. Tôi thấy nếu tôi có thể tìm thấy nó để bao gồm nó ở đây.
Bộ giải mã lười biếng

Phương pháp swizzling được biết đến là một giải pháp hack tinh tế cho các trường hợp hiếm hoi và niềm vui của nhà phát triển. Nó chắc chắn không an toàn cho App Store vì nó có khả năng phá vỡ và làm hỏng ứng dụng của bạn với bản cập nhật hệ điều hành tiếp theo. Tại sao không chỉ ghi đè pointInside: withEvent:?
auco

0

Nếu bạn không thể sử dụng một danh mục hoặc phân lớp UIView, bạn cũng có thể đưa nút về phía trước để nó ở phía trước chế độ xem trong suốt. Điều này sẽ không luôn luôn có thể tùy thuộc vào ứng dụng của bạn, nhưng nó hoạt động với tôi. Bạn luôn có thể mang nút trở lại một lần nữa hoặc ẩn nó.


2
Xin chào, chào mừng bạn đến với chồng tràn. Chỉ là một gợi ý cho bạn với tư cách là một người dùng mới: bạn có thể muốn cẩn thận với giọng điệu của mình. Tôi sẽ tránh các cụm từ như "nếu bạn không thể làm phiền"; nó có thể xuất hiện dưới dạng hạ thấp
nghĩa

Cảm ơn vì đã ngẩng cao đầu .. Đó là từ quan điểm lười biếng hơn là hạ mình, nhưng đã lấy điểm.
Lắc Sayag

0

Thử cái này

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

0

Hãy thử đặt một backgroundColorcủa bạn transparentViewnhư UIColor(white:0.000, alpha:0.020). Sau đó, bạn có thể nhận được các sự kiện chạm trong touchesBegan/ touchesMovedphương thức. Đặt mã bên dưới nơi nào đó chế độ xem của bạn được kích hoạt:

self.alpha = 1
self.backgroundColor = UIColor(white: 0.0, alpha: 0.02)
self.isMultipleTouchEnabled = true
self.isUserInteractionEnabled = true

0

Tôi sử dụng thay vì ghi đè phương thức điểm (bên trong: CGPoint, với: UIEvent)

  override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard self.point(inside: point, with: event) else { return nil }
        return self
    }
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.