ฉันจะคลิกปุ่มที่อยู่ด้านหลังโปร่งใสของ UIView ได้อย่างไร


177

สมมติว่าเรามีตัวควบคุมมุมมองที่มีหนึ่งมุมมองย่อย มุมมองย่อยใช้ศูนย์กลางของหน้าจอโดยมีระยะขอบ 100 px ทุกด้าน จากนั้นเราจะเพิ่มสิ่งเล็ก ๆ น้อย ๆ มากมายให้คลิกในมุมมองย่อยนั้น เราใช้มุมมองย่อยเพื่อใช้ประโยชน์จากเฟรมใหม่เท่านั้น (x = 0, y = 0 ภายในมุมมองย่อยเป็นจริง 100,100 ในมุมมองพาเรนต์)

จากนั้นจินตนาการว่าเรามีบางสิ่งที่อยู่ด้านหลังมุมมองย่อยเช่นเมนู ฉันต้องการให้ผู้ใช้สามารถเลือก "สิ่งเล็ก ๆ น้อย ๆ " ในมุมมองย่อยได้ แต่ถ้าไม่มีสิ่งใดฉันต้องการให้แตะผ่าน (เนื่องจากพื้นหลังมีความชัดเจนอยู่แล้ว) ไปยังปุ่มที่อยู่ด้านหลัง

ฉันจะทำสิ่งนี้ได้อย่างไร ดูเหมือนว่า touchegegan จะผ่าน แต่ปุ่มไม่ทำงาน


1
ฉันคิดว่า UIViews แบบโปร่งใส (อัลฟา 0) ไม่ควรตอบสนองต่อกิจกรรมแตะ
Evadne Wu

1
ฉันเขียนชั้นเรียนเล็ก ๆ เพื่อเรื่องนั้น (เพิ่มตัวอย่างในคำตอบ) วิธีแก้ปัญหานั้นค่อนข้างดีกว่าคำตอบที่ยอมรับเพราะคุณยังสามารถคลิกUIButtonที่อยู่ภายใต้กึ่งโปร่งใสUIViewในขณะที่ส่วนที่ไม่โปร่งใสของUIViewจะยังคงตอบสนองต่อเหตุการณ์ที่สัมผัส
Segev

คำตอบ:


315

สร้างมุมมองที่กำหนดเองสำหรับคอนเทนเนอร์ของคุณและแทนที่ pointInside: ข้อความเพื่อส่งกลับค่า false เมื่อจุดไม่อยู่ในมุมมองลูกที่มีสิทธิ์เช่นนี้

สวิฟท์:

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

วัตถุประสงค์ 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

การใช้มุมมองนี้เป็นที่เก็บจะอนุญาตให้ลูก ๆ ของมันรับสัมผัส แต่มุมมองนั้นจะโปร่งใสต่อเหตุการณ์


4
น่าสนใจ ฉันต้องดำน้ำในห่วงโซ่การตอบกลับมากขึ้น
Sean Clark Hess

1
คุณต้องตรวจสอบว่ามุมมองนั้นสามารถมองเห็นได้เช่นกันจากนั้นคุณต้องตรวจสอบว่ามีการดูย่อยหรือไม่ก่อนที่จะทำการทดสอบ
Lazy Coder

2
น่าเสียดายที่เคล็ดลับนี้ใช้ไม่ได้กับ tabBarController ซึ่งไม่สามารถเปลี่ยนมุมมองได้ ใครมีความคิดที่จะทำให้มุมมองนี้โปร่งใสต่อเหตุการณ์เช่นกัน?
cyrilchampier

1
คุณควรตรวจสอบอัลฟาของมุมมองด้วย บ่อยครั้งที่ฉันจะซ่อนมุมมองโดยการตั้งค่าอัลฟ่าเป็นศูนย์ มุมมองที่มีค่า alpha zero ควรทำหน้าที่เหมือนกับมุมมองที่ซ่อน
James Andrews

2
ฉันใช้เวอร์ชันที่รวดเร็ว: บางทีคุณสามารถรวมไว้ในคำตอบสำหรับการมองเห็นgist.github.com/eyeballz/17945454447c7ae766cb
eyeballz

107

ฉันยังใช้

myView.userInteractionEnabled = NO;

ไม่ต้องการคลาสย่อย ทำงานได้ดี


76
สิ่งนี้จะปิดใช้งานการโต้ตอบของผู้ใช้สำหรับการดูย่อย
pixelfreak

ดังที่ @pixelfreak พูดถึงนี่ไม่ใช่ทางเลือกที่เหมาะสำหรับทุกกรณี กรณีที่ต้องการให้มีการโต้ตอบกับมุมมองย่อยนั้นจำเป็นต้องใช้แฟล็กนั้นเพื่อเปิดใช้งาน
Augusto Carmo

20

จาก Apple:

การส่งต่อเหตุการณ์เป็นเทคนิคที่ใช้โดยบางแอปพลิเคชัน คุณส่งต่อกิจกรรมการสัมผัสโดยเรียกใช้วิธีการจัดการเหตุการณ์ของวัตถุตอบกลับอื่น แม้ว่านี่อาจเป็นเทคนิคที่มีประสิทธิภาพ แต่คุณควรใช้ด้วยความระมัดระวัง คลาสของเฟรมเวิร์ก UIKit ไม่ได้ถูกออกแบบมาเพื่อรับการสัมผัสที่ไม่ผูกพันกับพวกเขา .... หากคุณต้องการส่งต่อการสัมผัสไปยังผู้เผชิญเหตุอื่น ๆ ในแอปพลิเคชันของคุณอย่างมีเงื่อนไข

แนวปฏิบัติที่ดีที่สุดของ Apple :

อย่าส่งเหตุการณ์ที่เกิดขึ้นในห่วงโซ่การตอบกลับอย่างชัดเจน (ผ่าน nextResponder); ให้เรียกใช้งานซูเปอร์คลาสและปล่อยให้ UIKit จัดการการตอบกลับผ่านทางลูกโซ่

คุณสามารถแทนที่:

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

ในคลาสย่อย UIView ของคุณและส่งคืน NO หากคุณต้องการให้ระบบสัมผัสนั้นส่งสายการตอบกลับ (IE ไปยังมุมมองด้านหลังมุมมองของคุณโดยไม่มีสิ่งใดอยู่)


1
มันยอดเยี่ยมมากที่มีลิงค์ไปยังเอกสารที่คุณอ้างพร้อมกับคำพูดด้วยตัวเอง
morgancodes

1
ฉันเพิ่มลิงก์ไปยังสถานที่ที่เกี่ยวข้องในเอกสารสำหรับคุณ
jackslash

1
~~ คุณสามารถแสดงให้เห็นว่าการแทนที่นั้นจะต้องเป็นอย่างไร ~~ พบคำตอบในคำถามอื่นว่าลักษณะนี้จะเป็นอย่างไร มันเป็นเพียงแค่ตัวอักษรกลับNO;
Kontur

นี่อาจเป็นคำตอบที่ยอมรับได้ มันเป็นวิธีที่ง่ายที่สุดและวิธีที่แนะนำ
เอกวาดอร์

8

วิธีที่ง่ายกว่านั้นคือการยกเลิกการโต้ตอบผู้ใช้ที่เปิดใช้งานในเครื่องมือสร้างอินเตอร์เฟส "ถ้าคุณใช้กระดานเรื่องราว"

ป้อนคำอธิบายรูปภาพที่นี่


6
ตามที่คนอื่น ๆ ชี้ให้เห็นในคำตอบที่คล้ายกันสิ่งนี้ไม่ได้ช่วยในกรณีนี้เพราะมันยังปิดการใช้งานกิจกรรมการสัมผัสในมุมมองพื้นฐาน กล่าวอีกนัยหนึ่ง: ปุ่มด้านล่างมุมมองนั้นไม่สามารถแตะได้ไม่ว่าคุณจะเปิดหรือปิดการตั้งค่านี้
auco

6

สร้างสิ่งที่ John โพสต์ต่อไปนี้เป็นตัวอย่างที่จะช่วยให้เหตุการณ์การสัมผัสผ่านมุมมองย่อยทั้งหมดของมุมมองยกเว้นปุ่ม:

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

จำเป็นต้องตรวจสอบดูว่ามองเห็นได้หรือไม่และมองเห็นวิวย่อยหรือไม่
Coder Lazy

โซลูชั่นที่สมบูรณ์แบบ! วิธีการที่เรียบง่ายกว่าคือการสร้างUIViewคลาสย่อยPassThroughViewที่จะแทนที่-(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; }ถ้าคุณต้องการส่งต่อกิจกรรมการสัมผัสใด ๆ ไปยังมุมมองด้านล่าง
auco

6

เมื่อเร็ว ๆ นี้ฉันเขียนชั้นเรียนที่จะช่วยฉันด้วย ใช้เป็นคลาสแบบกำหนดเองสำหรับ a UIButtonหรือUIViewจะผ่านเหตุการณ์การสัมผัสที่ดำเนินการบนพิกเซลโปร่งใส

วิธีนี้เป็นคำตอบที่ดีกว่าคำตอบที่ยอมรับเพราะคุณยังสามารถคลิกUIButtonที่อยู่ภายใต้กึ่งโปร่งใสUIViewในขณะที่ส่วนที่ไม่โปร่งใสของUIViewจะยังคงตอบสนองต่อเหตุการณ์ที่สัมผัส

GIF

อย่างที่คุณเห็นใน GIF ปุ่ม Giraffe เป็นรูปสี่เหลี่ยมผืนผ้าที่เรียบง่าย แต่กิจกรรมการสัมผัสในพื้นที่โปร่งใสถูกส่งผ่านไปยังสีเหลืองที่UIButtonอยู่ด้านล่าง

เชื่อมโยงไปยังชั้นเรียน


1
ในขณะที่วิธีนี้อาจใช้งานได้ดีกว่าที่จะวางส่วนที่เกี่ยวข้องของโซลูชันของคุณไว้ในคำตอบ
โคเอ็น

4

โซลูชันที่ได้รับคะแนนสูงสุดไม่ทำงานอย่างเต็มที่สำหรับฉันฉันคิดว่ามันเป็นเพราะฉันมี TabBarController เป็นลำดับชั้น (เป็นหนึ่งในความคิดเห็นที่ชี้ให้เห็น) ในความเป็นจริงผ่านไปสัมผัสกับบางส่วนของ UI แต่มันยุ่งกับ ความสามารถของ tableView ในการสกัดกั้นเหตุการณ์การสัมผัสสิ่งที่สุดท้ายก็คือการเอาชนะhitTestในมุมมองที่ฉันต้องการละเว้นการสัมผัสและให้ภาพย่อยของมุมมองนั้นจัดการกับพวกเขา

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

สวิฟท์ 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

ตาม 'คู่มือการเขียนโปรแกรมแอปพลิเคชัน iPhone':

ปิดการส่งกิจกรรมการสัมผัส ตามค่าเริ่มต้นมุมมองจะได้รับเหตุการณ์การสัมผัส แต่คุณสามารถตั้งค่าคุณสมบัติ userInteractionEnabled เป็น NO เพื่อปิดการส่งกิจกรรม มุมมองที่ยังไม่ได้รับเหตุการณ์ที่เกิดขึ้นถ้ามันซ่อนอยู่หรือถ้ามันโปร่งใส

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

อัปเดต : ตัวอย่างที่ลบ - อ่านคำถามอีกครั้ง ...

คุณมีการประมวลผลท่าทางในมุมมองที่อาจประมวลผลการแตะก่อนที่ปุ่มจะได้รับหรือไม่? ปุ่มทำงานเมื่อคุณไม่มีมุมมองที่โปร่งใสหรือไม่

ตัวอย่างโค้ดใด ๆ ของโค้ดที่ไม่ทำงาน?


ใช่ฉันเขียนในคำถามของฉันว่าสัมผัสปกติทำงานภายใต้มุมมอง แต่ UIButtons และองค์ประกอบอื่น ๆ ไม่ทำงาน
Sean Clark Hess

1

เท่าที่ฉันรู้คุณควรจะสามารถทำได้โดยการแทนที่hitTest: method ฉันลองแล้ว แต่ไม่สามารถทำงานได้อย่างถูกต้อง

ในตอนท้ายฉันสร้างชุดของมุมมองแบบโปร่งใสรอบ ๆ วัตถุที่สัมผัสได้เพื่อไม่ให้ครอบคลุม บิตของแฮ็คสำหรับปัญหาของฉันทำงานได้ดี


1

รับเคล็ดลับจากคำตอบอื่น ๆ และอ่านเอกสารของ Apple ฉันได้สร้างไลบรารีง่าย ๆ สำหรับแก้ปัญหาของคุณ:
https://github.com/natrosoft/NATouchThroughView
มันทำให้ง่ายต่อการวาดมุมมองในตัวสร้างส่วนติดต่อที่ควรผ่านการสัมผัสกับ มุมมองพื้นฐาน

ฉันคิดว่าวิธีการ swizzling นั้นเกินความจริงและอันตรายมากที่ต้องทำในรหัสการผลิตเพราะคุณกำลังยุ่งกับการติดตั้งฐานของ Apple โดยตรงและทำให้เกิดการเปลี่ยนแปลงทั่วทั้งแอปพลิเคชันที่อาจทำให้เกิดผลที่ไม่ตั้งใจ

มีโครงการตัวอย่างและหวังว่า README จะทำงานได้ดีอธิบายว่าต้องทำอย่างไร ในการจัดการกับ OP คุณจะต้องเปลี่ยน UIView ที่ชัดเจนที่มีปุ่มเป็นคลาส NATouchThroughView ในเครื่องมือสร้างส่วนต่อประสาน จากนั้นค้นหา UIView ที่ชัดเจนซึ่งซ้อนทับเมนูที่คุณต้องการให้แตะได้ เปลี่ยน UIView นั้นเป็นคลาส NARootTouchThroughView ใน Interface Builder มันอาจเป็นรูท UIView ของตัวควบคุมมุมมองของคุณหากคุณต้องการให้ผู้ใช้สัมผัสเพื่อผ่านไปยังตัวควบคุมมุมมองพื้นฐาน ลองดูตัวอย่างโครงการเพื่อดูว่ามันทำงานอย่างไร มันค่อนข้างง่ายปลอดภัยและไม่รุกราน


1

ฉันสร้างหมวดหมู่เพื่อทำสิ่งนี้

วิธีเล็ก ๆ น้อย ๆ และมุมมองที่เป็นสีทอง

ส่วนหัว

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

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

@end

ไฟล์การนำไปใช้งาน

#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

คุณจะต้องเพิ่ม Swizz.h และ Swizz.m ของฉัน

ตั้งอยู่ที่นี่

หลังจากนั้นคุณเพียงนำเข้า UIView + PassthroughParent.h ในไฟล์ {Project} -Prefix.pch ของคุณและทุกมุมมองจะมีความสามารถนี้

ทุกมุมมองจะได้รับคะแนน แต่ไม่มีพื้นที่ว่างที่จะ

ฉันขอแนะนำให้ใช้พื้นหลังที่ชัดเจน

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

แก้ไข

ฉันสร้างถุงสมบัติของฉันเองและไม่ได้รวมไว้ก่อนหน้านี้

ไฟล์ส่วนหัว

// NSObject+PropertyBag.h

#import <Foundation/Foundation.h>

@interface NSObject (PropertyBag)

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

@end

ไฟล์การใช้งาน

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

วิธี passthroughPointInside ทำงานได้ดีสำหรับฉัน (แม้จะไม่ใช้ passthroughparent หรือ swizz อย่างใดอย่างหนึ่ง - เพียงแค่เปลี่ยนชื่อ passthroughPointInside เป็น pointInside) ขอบคุณมาก
Benjamin Piette

propertyValueForKey คืออะไร
Skotch

1
อืมมม ไม่มีใครชี้ให้เห็นก่อนหน้านี้ ฉันสร้างพจนานุกรมตัวชี้แบบกำหนดเองที่เก็บคุณสมบัติไว้ในส่วนขยายคลาส ฉันจะดูว่าฉันสามารถหามันเพื่อรวมไว้ที่นี่ได้ไหม
Coder Lazy

วิธีการ Swizzling เป็นที่รู้จักกันว่าเป็นวิธีการแฮ็คที่ละเอียดอ่อนสำหรับกรณีที่หายากและความสนุกสนานของนักพัฒนา แน่นอนว่ามันไม่ปลอดภัยใน App Store เพราะมีแนวโน้มที่จะทำลายและทำให้แอปของคุณเสียหายด้วยการอัปเดตระบบปฏิบัติการครั้งต่อไป ทำไมไม่ลองเขียนทับดูpointInside: withEvent:ล่ะ?
auco

0

หากคุณไม่สามารถใช้หมวดหมู่หรือคลาสย่อย UIView คุณสามารถนำปุ่มไปข้างหน้าเพื่อให้อยู่หน้ามุมมองแบบโปร่งใส สิ่งนี้อาจเป็นไปไม่ได้ขึ้นอยู่กับแอปพลิเคชันของคุณ แต่มันได้ผลสำหรับฉัน คุณสามารถนำปุ่มกลับมาได้อีกครั้งหรือซ่อนไว้


2
สวัสดียินดีต้อนรับสู่สแต็คล้น เพียงคำแนะนำสำหรับคุณในฐานะผู้ใช้ใหม่: คุณอาจต้องระวังโทนของคุณ ฉันจะหลีกเลี่ยงวลีเช่น "ถ้าคุณไม่สามารถรบกวน"; มันอาจปรากฏว่าเป็นการวางตัว
nomistic

ขอบคุณสำหรับหัวขึ้น .. มันเป็นมากกว่าจุดยืนที่ขี้เกียจกว่าวางตัว แต่จุดที่
Shaked Sayag

0

ลองสิ่งนี้

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

ลองชุดbackgroundColorที่คุณเป็นtransparentView UIColor(white:0.000, alpha:0.020)จากนั้นคุณสามารถรับเหตุการณ์สัมผัสในtouchesBegan/ touchesMovedวิธี วางโค้ดด้านล่างบางมุมมองที่คุณมีอยู่:

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

0

ฉันใช้สิ่งนั้นแทนการแทนที่เมธอด point (ภายใน: CGPoint ด้วย: UIEvent)

  override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard self.point(inside: point, with: event) else { return nil }
        return self
    }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.