Permitindo a interação com um UIView em outro UIView

115

Existe uma maneira simples de permitir a interação com um botão em um UIView que fica sob outro UIView - onde não há objetos reais do UIView superior na parte superior do botão?

Por exemplo, no momento eu tenho um UIView (A) com um objeto na parte superior e um objeto na parte inferior da tela e nada no meio. Ele fica em cima de outro UIView que tem botões no meio (B). No entanto, não consigo interagir com os botões no meio de B.

Eu posso ver os botões em B - eu defini o plano de fundo de A para clearColor - mas os botões em B não parecem receber toques, apesar do fato de que não há objetos de A realmente no topo desses botões.

EDITAR - Eu ainda quero ser capaz de interagir com os objetos no UIView superior

Certamente existe uma maneira simples de fazer isso?

Delany
fonte
2
É praticamente tudo explicado aqui: developer.apple.com/iphone/library/documentation/iPhone/… Mas basicamente, sobrescrever hitTest: withEvent :, eles até fornecem uma amostra de código.
nash
Eu escrevi uma pequena classe 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 um UIButtonque está sob uma semitransparente, UIViewenquanto a parte não transparente do UIViewainda responderá aos eventos de toque.
Segev

Respostas:

97

Você deve criar uma subclasse UIView para sua vista superior e substituir o seguinte método:

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

Você também pode consultar o método hitTest: event:.

Gyim
fonte
19
O que middle_y1 / y2 está representando aqui?
Jason Renaldo
Não tenho certeza do que essa returndeclaração está fazendo, mas return CGRectContainsPoint(eachSubview.frame, point)funciona para mim. Resposta extremamente útil caso contrário
n00neimp0rtant
O negócio MIDDLE_Y1 / Y2 é apenas um exemplo. Esta função será "transparente" para eventos de toque na MIDDLE_Y1<=y<=MIDDLE_Y2área.
Gyim
41

Embora muitas das respostas aqui funcionem, estou um pouco surpreso ao ver que a resposta mais conveniente, genérica e infalível não foi dada aqui. @Ash chegou mais perto, exceto que há algo estranho acontecendo com o retorno do superview ... não faça isso.

Esta resposta foi retirada de uma resposta que dei a uma pergunta semelhante, aqui .

- (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]retornará a visão mais profunda na hierarquia dessa visão que foi tocada. Se hitView == self(ou seja, se não houver subvisualização sob o ponto de toque), retorne nil, especificando que esta visualização não deve receber o toque. A maneira como a cadeia de resposta funciona significa que a hierarquia de visualização acima desse ponto continuará a ser percorrida até que seja encontrada uma visualização que responda ao toque. Não devolva o superview, pois não cabe a ele se o superview deve aceitar toques ou não!

Esta solução é:

  • conveniente , porque não requer referências a quaisquer outras visualizações / subvisualizações / objetos;
  • genérico , porque se aplica a qualquer visualização que atua puramente como um contêiner para subvisualizações tocáveis, e a configuração das subvisualizações não afeta a maneira como funciona (como acontece se você substituir pointInside:withEvent:para retornar uma área tocável específica).
  • à prova de falhas , não há muito código ... e o conceito não é difícil de entender.

Eu uso isso com freqüência suficiente para abstrair em uma subclasse para salvar subclasses de visão sem sentido para uma substituição. Como bônus, adicione uma propriedade para torná-la configurável:

@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

Então vá à loucura e use esta visualização onde quer que você possa usar uma planície UIView. Configurá-lo é tão simples quanto definir onlyRespondToTouchesInSubviewspara YES.

Stuart
fonte
2
A única resposta correta. para ser claro, se você deseja ignorar os toques em uma visualização, mas não em quaisquer botões (digamos) contidos na visualização , siga as instruções de Stuart. (Eu normalmente chamo de "visualização do suporte" porque pode "segurar" inofensivamente alguns botões, mas não afeta nada "abaixo" do suporte.)
Fattie
Algumas das outras soluções que usam pontos têm problemas se sua visualização for rolável, como UITableView og UICollectionView e você rolou para cima ou para baixo. Esta solução, entretanto, funciona independentemente da rolagem.
pajevic de
31

Existem várias maneiras de lidar com isso. Meu favorito é substituir hitTest: withEvent: em uma visão que é uma visão geral comum (talvez indiretamente) para as visões conflitantes (parece que você as chama de A e B). Por exemplo, algo assim (aqui A e B são ponteiros UIView, onde B é o "oculto", que normalmente é ignorado):

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

Você também pode modificar o pointInside:withEvent:método como gyim sugerido. Isso permite que você alcance essencialmente o mesmo resultado efetivamente "abrindo um buraco" em A, pelo menos para tocar.

Outra abordagem é o encaminhamento de evento, o que significa substituir touchesBegan:withEvent:métodos semelhantes (como touchesMoved:withEvent:etc) para enviar alguns toques para um objeto diferente daquele onde foram inicialmente. Por exemplo, em A, você poderia escrever algo assim:

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    if ([self shouldForwardTouches:touches]) {
        [B touchesBegan:touches withEvent:event];
    }
    else {
        // Do whatever A does with touches.
    }
}

No entanto, isso nem sempre funcionará da maneira que você espera! O principal é que os controles embutidos como o UIButton sempre irão ignorar os toques encaminhados. Por causa disso, a primeira abordagem é mais confiável.

Há uma boa postagem no blog explicando tudo isso com mais detalhes, junto com um pequeno projeto xcode funcional para demonstrar as ideias, disponível aqui:

http://bynomial.com/blog/?p=74

Tyler
fonte
O problema com esta solução - substituindo hitTest na supervisão comum - é que ela não permite que a visão inferior funcione totalmente corretamente quando a visão inferior é uma de um conjunto completo de exibições roláveis ​​(MapView etc). Substituir o ponto interno na vista superior, conforme sugerido por gyim abaixo, funciona em todos os casos, pelo que posso dizer.
delany
@delany, Isso não é verdade; você pode ter visualizações roláveis ​​abaixo de outras visualizações e permitir que ambas funcionem substituindo hitTest. Aqui está um exemplo de código: bynomial.com/blogfiles/Temp32.zip
Tyler,
Ei. Nem todas as visualizações roláveis ​​- apenas algumas ... Tentei seu CEP acima, alterando o UIScrollView para um (por exemplo) MKMapView e não funcionou. Os toques funcionam - é a rolagem que parece ser o problema.
delany,
Ok, eu verifiquei e confirmei que hitTest não funciona da maneira que você gostaria com MKMapView. Você estava certo sobre isso, @delany; embora funcione corretamente com o UIScrollView. Eu me pergunto por que MKMapView falha?
Tyler,
@Tyler Como você mencionou, que "O principal é que controles embutidos como o UIButton sempre ignorarão os toques encaminhados", quero saber como você sabe disso e se é um documento oficial para explicar esse comportamento. Eu enfrentei um problema que quando o UIButton tem uma subvisualização simples do UIView, ele não responde a eventos de toque nos limites da subvisualização. E descobri que o evento é encaminhado para o UIButton corretamente como o comportamento padrão do UIView, mas não tenho certeza se é um recurso designado ou apenas um bug. Você poderia me mostrar a documentação sobre isso? Muito obrigado, sinceramente.
Neal.Marlin
29

Você tem que definir upperView.userInteractionEnabled = NO;, caso contrário, a vista superior interceptará os toques.

A versão do Interface Builder é uma caixa de seleção na parte inferior do painel View Attributes chamada "User Interaction Enabled". Desmarque-a e você estará pronto para ir.

Benjamin Cox
fonte
Desculpe - deveria ter dito. Ainda quero ser capaz de interagir com os objetos no UIView superior.
delany
Mas o upperView não pode receber nenhum toque, inclua o botão no upperView.
imcaptor
2
Esta solução funciona para mim. Eu não precisava da visão superior para reagir aos toques.
TJ de
11

Implementação personalizada de pointInside: withEvent: de fato parecia o caminho a seguir, mas lidar com coordenadas codificadas parecia estranho para mim. Então acabei verificando se o CGPoint estava dentro do botão CGRect usando a função CGRectContainsPoint ():

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

Ultimamente eu escrevi uma aula que vai me ajudar exatamente nisso. Usando-o como uma classe personalizada para um UIButtonouUIView irá passar eventos de toque que foram executados em um pixel transparente.

Esta solução é um pouco melhor do que a resposta aceita porque você ainda pode clicar em um UIButtonque está sob um semitransparente, UIViewenquanto a parte não transparente do 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 nas áreas transparentes são passados ​​para o amarelo UIButtonabaixo.

Link para a aula

Segev
fonte
2
Já que seu código não é tão longo, você deve incluir as partes relevantes dele em sua resposta, caso seu projeto seja movido ou removido em algum momento no futuro.
Gavin
Obrigado, sua solução funciona para o meu caso onde eu preciso da parte não transparente do meu UIView para responder a eventos de toque, enquanto a parte transparente não. Brilhante!
Bruce
@Bruce Que bom que ajudou você!
Segev
4

Acho que estou um pouco atrasado para esta festa, mas acrescentarei esta possível solução:

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

Se você usar este código para substituir a função hitTest padrão de um UIView personalizado, ele ignorará SOMENTE a própria visualização. Quaisquer subvisualizações dessa visão retornarão seus resultados normalmente, e quaisquer acessos que teriam ido para a própria visão são passados ​​para sua supervisão.

-Cinza

Cinza
fonte
1
este é o meu método preferido, exceto, eu não acho que você deveria retornar [self superview]. a documentação sobre este método afirma "Retorna o descendente mais distante do receptor na hierarquia de exibição (incluindo ele mesmo) que contém um ponto especificado" e "Retorna nulo se o ponto estiver completamente fora da hierarquia de exibição do receptor". Eu acho que você deveria voltar nil. quando você retornar nil, o controle passará para o supervisualizador para que ele verifique se há ocorrências ou não. então, basicamente, ele fará a mesma coisa, exceto que retornar o superview pode interromper algo no futuro.
jasongregori
Claro, isso provavelmente seria sensato (observe a data na minha resposta original - fiz muito mais codificação desde então)
Ash
4

Estou apenas revisando a Resposta Aceita e colocando isso aqui para minha referência. A resposta aceita funciona perfeitamente. Você pode estendê-lo assim para permitir que as subvisualizações de sua visualização recebam o toque OU passá-lo para quaisquer visualizações atrás de nós:

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

Nota: Você nem mesmo precisa fazer recursão na árvore de subvisualização, porque cada pointInside:withEvent:método cuidará disso para você.

Dan Rosenstark
fonte
3

A configuração da propriedade userInteraction desabilitada pode ajudar. Por exemplo:

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

(Observação: no código acima, 'self' se refere a uma visualização)

Dessa forma, você só pode exibir no topView, mas não obterá entradas do usuário. Todos os toques do usuário passarão por essa visualização e a visualização inferior responderá por eles. Eu usaria este topView para exibir imagens transparentes ou para animá-las.

Aditya
fonte
3

Essa abordagem é bastante limpa e permite que subvisualizações transparentes também não reajam aos toques. Basta UIViewcriar uma subclasse e adicionar o seguinte método à sua implementação:

@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
prodos
fonte
2

Minha solução aqui:

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

Espero que isto ajude

Jerry
fonte
2

Há algo que você pode fazer para interceptar o toque em ambas as visualizações.

Vista do topo:

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

Mas essa é a ideia.

Alexandre Cassagne
fonte
Isso é certamente possível - mas dá muito trabalho (você teria que rolar seus próprios objetos sensíveis ao toque na camada inferior (por exemplo, botões), eu acho?) E parece estranho que você tenha que rolar seus próprios objetos maneira de obter um comportamento que parece ser intuitivo.
delany
Não tenho certeza se devemos apenas tentar.
Alexandre Cassagne
2

Aqui está uma versão Swift:

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

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
}
pyRabbit
fonte
1

Nunca construí uma interface de usuário completa usando o kit de ferramentas de IU, então não tenho muita experiência com ele. Aqui está o que eu acho que deve funcionar.

Cada UIView, e esta a UIWindow, tem uma propriedade subviews , que é um NSArray contendo todas as subvisualizações.

A primeira subvisualização adicionada a uma visão receberá o índice 0, e a próxima índice 1 e assim por diante. Você também pode substituir addSubview:por insertSubview: atIndex:ouinsertSubview:aboveSubview: e esses métodos que podem determinar a posição de sua subvisão na hierarquia.

Portanto, verifique seu código para ver qual visualização você adiciona primeiro à sua UIWindow. Esse será 0, o outro será 1.
Agora, a partir de uma de suas subvisões, para chegar a outra, você faria o seguinte:

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

Deixe-me saber se isso funciona para o seu caso!


(abaixo deste marcador está minha resposta anterior):

Se as visualizações precisam se comunicar umas com as outras, elas devem fazer isso por meio de um controlador (ou seja, usando o modelo MVC popular ).

Ao criar uma nova visualização, você pode ter certeza de que ela se registrará em um controlador.

Portanto, a técnica é garantir que suas visualizações sejam registradas em um controlador (que pode armazená-las por nome ou o que você preferir em um Dicionário ou Array). Você pode fazer com que o controlador envie uma mensagem para você ou pode obter uma referência para a visualização e se comunicar diretamente com ela.

Se sua visão não tem um link de volta para o controlador (o que pode ser o caso), então você pode usar singletons e / ou métodos de classe para obter uma referência para seu controlador.

nash
fonte
Obrigado pela sua resposta - mas não tenho certeza se entendi. Ambas as visualizações têm um controlador - o problema é que, com uma visualização em cima da outra, a visualização inferior não coleta eventos (e os encaminha para seu controlador), mesmo que não haja nenhum objeto realmente "bloqueando" esses eventos em a vista de cima.
delany
Você tem uma UIWindow no topo de nossas UIViews? Se você fizer isso, os eventos deverão ser propagados e você não precisará fazer nenhuma "mágica". Leia sobre [Window and Views] [1] no Apple Dev Center (e por suposto, adicione outro comentário se isso não ajudar você!) [1]: developer.apple.com/iphone/library/documentation/ iPhone /…
nash de
Sim, absolutamente - uma UIWindow no topo da hierarquia.
delany
Atualizei minha resposta para incluir o código para percorrer uma hierarquia de visualizações. Deixe-me saber se você precisar de qualquer outra ajuda!
nash
Obrigado por sua ajuda nash - mas tenho bastante experiência em construir essas interfaces e a minha atual está configurada corretamente pelo que eu sei. O problema parece ser o comportamento padrão das visualizações completas quando colocadas em cima das outras.
delany
1

Acho que a maneira certa é usar a cadeia de visualização incorporada à hierarquia de visualização. Para suas subvisualizações que são enviadas para a exibição principal, não use o UIView genérico, mas, em vez disso, subclasse UIView (ou uma de suas variantes como UIImageView) para fazer MYView: UIView (ou qualquer supertipo que você desejar, como UIImageView). Na implementação de YourView, implemente o método touchesBegan. Este método será invocado quando essa visualização for tocada. Tudo que você precisa ter nessa implementação é um método de instância:

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

este touchBegan é uma api respondente, então você não precisa declará-lo em sua interface pública ou privada; é uma daquelas APIs mágicas que você precisa conhecer. Este self.superview irá borbulhar a solicitação eventualmente para o viewController. No viewController, então, implemente este touchesBegan para lidar com o toque.

Observe que a localização dos toques (CGPoint) é automaticamente ajustada em relação à visão abrangente para você, à medida que é rebatida para cima na cadeia de hierarquia da visão.

PapaSmurf
fonte
1

Só quero postar isso, porque eu tive um problema semelhante, passei uma quantidade substancial de tempo tentando implementar respostas aqui sem sorte. O que acabei fazendo:

 for(UIGestureRecognizer *recognizer in topView.gestureRecognizers)
 {
     recognizer.delegate=self;
     [bottomView addGestureRecognizer:recognizer];   
 }
 topView.abView.userInteractionEnabled=NO; 

e implementando UIGestureRecognizerDelegate:

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
    return YES;
}

A vista inferior era um controlador de navegação com um número de segues e eu tinha uma espécie de porta em cima dele que poderia fechar com o gesto panorâmico. A coisa toda estava embutida em outro VC. Funcionou como um encanto. Espero que isto ajude.

stringCode
fonte
1

Implementação de Swift 4 para solução baseada em HitTest

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

Derivado da resposta excelente e principalmente infalível de Stuart e da implementação útil do Segev, aqui está um pacote Swift 4 que você pode colocar em qualquer projeto:

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

E então com 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)
}
brandonscript
fonte