Como posso clicar em um botão atrás de um UIView transparente?

176

Digamos que temos um controlador de exibição com uma subvista. a subvisão ocupa o centro da tela com margens de 100 px em todos os lados. Em seguida, adicionamos um monte de pequenas coisas para clicar dentro dessa subview. Estamos apenas usando a subvisão para tirar proveito do novo quadro (x = 0, y = 0 dentro da subvisão é realmente 100.100 na visualização pai).

Então, imagine que temos algo por trás da subvisão, como um menu. Eu quero que o usuário possa selecionar qualquer uma das "pequenas coisas" na subvisão, mas se não houver nada lá, desejo que passem por ela (desde que o fundo esteja claro de qualquer maneira) para os botões atrás dela.

Como posso fazer isso? Parece que o touchesBegan passa, mas os botões não funcionam.

Sean Clark Hess
fonte
1
Eu pensei que UIViews transparentes (alpha 0) não deveriam responder a eventos de toque?
Evadne Wu
1
Eu escrevi uma turma pequena apenas para isso. (Adicionado um exemplo nas respostas). A solução é um pouco melhor do que a resposta aceita, porque você ainda pode clicar em uma UIButtonque esteja semitransparente UIViewenquanto a parte não transparente UIViewainda responderá a eventos de toque.
Segev

Respostas:

314

Crie uma exibição personalizada para seu contêiner e substitua a mensagem pointInside: para retornar false quando o ponto não estiver em uma exibição filho qualificada, como esta:

Rápido:

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

Objetivo 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

O uso dessa visualização como um contêiner permitirá que qualquer um de seus filhos receba toques, mas a própria visualização será transparente para os eventos.

John Stephen
fonte
4
Interessante. Eu preciso mergulhar mais na cadeia de respostas.
Sean Clark Hess
1
você precisa verificar se a visualização também está visível e também verificar se as subvisões estão visíveis antes de testá-las.
The Lazy Coder
2
infelizmente, esse truque não funciona para um tabBarController, para o qual a exibição não pode ser alterada. Alguém tem uma ideia de tornar essa visão transparente para eventos também?
Cyrilchampier 15/08/12
1
Você também deve verificar o alfa da visualização. Muitas vezes, ocultarei uma visualização definindo o alfa como zero. Uma exibição com zero alfa deve agir como uma exibição oculta.
James Andrews
2
Eu fiz uma rápida Versão: talvez você pode incluí-lo na resposta para a visibilidade gist.github.com/eyeballz/17945454447c7ae766cb
EYEBALLZ
107

Eu também uso

myView.userInteractionEnabled = NO;

Não há necessidade de subclasse. Funciona bem.

akw
fonte
76
Isso também irá desativar a interação do usuário para qualquer subviews
pixelfreak
Como o @pixelfreak mencionou, essa não é a solução ideal para todos os casos. Os casos em que a interação nas subvisões dessa visão ainda são desejados exigem que o sinalizador permaneça ativado.
Augusto Carmo
20

Da Apple:

O encaminhamento de eventos é uma técnica usada por alguns aplicativos. Você encaminha eventos de toque invocando os métodos de manipulação de eventos de outro objeto de resposta. Embora essa possa ser uma técnica eficaz, você deve usá-la com cuidado. As classes da estrutura do UIKit não foram projetadas para receber toques que não estão vinculados a eles .... Se você deseja encaminhar condicionalmente toques para outros respondentes no seu aplicativo, todos esses respondedores devem ser instâncias de suas próprias subclasses do UIView.

Prática recomendada de maçãs :

Não envie eventos explicitamente pela cadeia de respostas (via nextResponder); invoque a implementação da superclasse e deixe o UIKit manipular a travessia da cadeia de respostas.

em vez disso, você pode substituir:

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

na sua subclasse UIView e retorne NO se desejar que esse toque seja enviado para a cadeia de respostas (ou seja, para visualizações por trás da visualização sem nada).

macacão
fonte
1
Seria incrível ter um link para os documentos que você está citando, além das próprias aspas.
12132 morgancodes
1
Eu adicionei os links para os lugares relevantes na documentação para você
jackslash
1
~~ Você poderia mostrar como essa substituição teria que ser? ~~ Encontrei uma resposta em outra pergunta de como isso seria; está literalmente apenas retornando NO;
kontur
Provavelmente essa deve ser a resposta aceita. É a maneira mais simples e recomendada.
Equador
8

Uma maneira muito mais simples é "Desmarcar" a interação do usuário ativada no construtor de interfaces. "Se você estiver usando um storyboard"

insira a descrição da imagem aqui

Jeff
fonte
6
como outros apontaram em uma resposta semelhante, isso não ajuda nesse caso, pois também está desativando os eventos de toque na visualização subjacente. Em outras palavras: o botão abaixo dessa visualização não pode ser tocado, independentemente de você ativar ou desativar essa configuração.
auco 4/08/16
6

Com base no que João postou, aqui está um exemplo que permitirá que os eventos de toque passem por todas as subvisões de uma exibição, exceto pelos botões:

-(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;
}
Tod Cunningham
fonte
É necessário verificar se a visualização está visível e se as subvisões estão visíveis.
The Lazy Coder
Solução perfeita! Uma abordagem ainda mais simples seria criar uma UIViewsubclasse PassThroughViewque apenas substitui -(BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { return YES; }se você deseja encaminhar eventos de toque para as vistas abaixo.
auco 4/08/16
6

Ultimamente, escrevi uma aula que vai me ajudar com isso. Usá-lo como uma classe personalizada para um UIButtonou UIViewpassará por eventos de toque que foram executados em um pixel transparente.

Essa solução é um pouco melhor do que a resposta aceita porque você ainda pode clicar em uma UIButtonque esteja semitransparente UIViewenquanto a parte não transparente UIViewainda responderá a eventos de toque.

GIF

Como você pode ver no GIF, o botão Giraffe é um retângulo simples, mas os eventos de toque em áreas transparentes são passados ​​para o amarelo UIButtonabaixo.

Link para a aula

Segev
fonte
1
Embora essa solução possa funcionar, é melhor colocar as partes relevantes da sua solução na resposta.
Koen.
4

A solução mais votada não estava funcionando totalmente para mim, acho que era porque eu tinha um TabBarController na hierarquia (como um dos comentários assinala). A capacidade do tableView de interceptar eventos de toque, o que finalmente fez foi substituir o hitTest na exibição que eu quero ignorar os toques e deixar que as subvisões dessa exibição os manipulem

- (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
    }
}
PakitoV
fonte
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
}
Barath
fonte
2

De acordo com o 'Guia de programação de aplicativos do iPhone':

Desativando a entrega de eventos de toque. Por padrão, uma exibição recebe eventos de toque, mas você pode definir sua propriedade userInteractionEnabled como NO para desativar a entrega de eventos. Uma exibição também não recebe eventos se estiver oculta ou transparente .

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

Atualizado : exemplo removido - reler a pergunta ...

Você tem algum processamento de gesto nas visualizações que podem estar processando os toques antes que o botão atinja? O botão funciona quando você não tem uma visão transparente?

Algum exemplo de código que não funciona?

LK.
fonte
Sim, escrevi na minha pergunta que os toques normais funcionam sob as visualizações, mas os UIButtons e outros elementos não funcionam.
Sean Clark Hess
1

Até onde eu sei, você deve conseguir fazer isso substituindo o método hitTest:. Eu tentei, mas não consegui fazê-lo funcionar corretamente.

No final, criei uma série de visualizações transparentes ao redor do objeto tocável, para que não o cobrissem. Um pouco de um truque para o meu problema funcionou bem.

Liam
fonte
1

Tomando dicas das outras respostas e lendo a documentação da Apple, criei esta biblioteca simples para solucionar seu problema:
https://github.com/natrosoft/NATouchThroughView
Torna fácil desenhar visualizações no Interface Builder que devem passar detalhes para uma visão subjacente.

Acho que o método swizzling é um exagero e muito perigoso de fazer no código de produção, porque você está mexendo diretamente com a implementação básica da Apple e fazendo uma alteração em todo o aplicativo que pode causar conseqüências não intencionais.

Existe um projeto de demonstração e espero que o README faça um bom trabalho explicando o que fazer. Para abordar o OP, você alteraria o UIView claro que contém os botões para classificar NATouchThroughView no Interface Builder. Em seguida, encontre a UIView clara que se sobrepõe ao menu que você deseja ativar. Altere esse UIView para a classe NARootTouchThroughView no Interface Builder. Pode até ser a UIView raiz do seu controlador de exibição, se você deseja que esses toques passem para o controlador de exibição subjacente. Confira o projeto de demonstração para ver como ele funciona. É realmente muito simples, seguro e não invasivo

n8tr
fonte
1

Eu criei uma categoria para fazer isso.

um pequeno método swizzling e a vista é dourada.

O cabeçalho

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

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

@end

O arquivo de implementação

#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

Você precisará adicionar meus Swizz.he Swizz.m

localizado aqui

Depois disso, basta importar o UIView + PassthroughParent.h no seu arquivo {Project} -Prefix.pch e todas as visualizações terão essa capacidade.

toda visualização terá pontos, mas nenhum espaço em branco.

Eu também recomendo usar um fundo claro.

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

EDITAR

Criei minha própria sacola de propriedade e isso não foi incluído anteriormente.

Arquivo de cabeçalho

// NSObject+PropertyBag.h

#import <Foundation/Foundation.h>

@interface NSObject (PropertyBag)

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

@end

Arquivo de Implementação

// 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
The Lazy Coder
fonte
O método passthroughPointInside está funcionando bem para mim (mesmo sem usar nada do passthroughparent ou swizz - basta renomear passthroughPointInside para pointInside), muito obrigado.
Benjamin Piette 8/03
O que é propertyValueForKey?
21713 Skotch
1
Hmm. Ninguém apontou isso antes. Criei um dicionário de ponteiro personalizado que contém propriedades em uma extensão de classe. Vou ver se consigo encontrá-lo aqui.
The Lazy Coder
Os métodos swizzling são conhecidos por serem uma solução delicada de hackers para casos raros e diversão para os desenvolvedores. Definitivamente, não é seguro para a App Store, pois é muito provável que ele interrompa e trava o aplicativo com a próxima atualização do sistema operacional. Por que não substituir pointInside: withEvent:?
auco 4/08/16
0

Se você não puder usar um UIView de categoria ou subclasse, também poderá avançar o botão para que fique na frente da visualização transparente. Isso nem sempre será possível, dependendo da sua aplicação, mas funcionou para mim. Você sempre pode trazer o botão de volta ou ocultá-lo.

Sayag sacudido
fonte
2
Olá, bem-vindo ao estouro de pilha. Apenas uma dica para você como um novo usuário: você pode ter cuidado com o seu tom. Eu evitaria frases como "se você não puder se incomodar"; pode parecer condescendente
nomistic
Obrigado pelo aviso. Era mais do ponto de vista preguiçoso do que condescendente, mas foi o ponto.
Shaked Sayag
0

Tente isto

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
        }
    } 
tBug
fonte
0

Tente definir um backgroundColordos seus transparentViewcomo UIColor(white:0.000, alpha:0.020). Então você pode obter eventos de toque em touchesBegan/ touchesMovedmethods. Coloque o código abaixo em algum lugar da sua visualização:

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

Eu uso isso em vez de substituir o ponto do método (dentro: CGPoint, com: UIEvent)

  override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard self.point(inside: point, with: event) else { return nil }
        return self
    }
Wei
fonte