Obter o UIViewController a partir do UIView?

185

Existe uma maneira embutida de ir de um UIViewpara o seu UIViewController? Eu sei que você pode ir de UIViewControllersua UIViewvia, [self view]mas eu queria saber se existe uma referência reversa?

bertrandom
fonte

Respostas:

46

Como essa é a resposta aceita há muito tempo, sinto que preciso corrigi-la com uma resposta melhor.

Alguns comentários sobre a necessidade:

  • Sua visualização não precisa acessar diretamente o controlador de visualização.
  • A visualização deve ser independente do controlador de visualização e ser capaz de trabalhar em diferentes contextos.
  • Se você precisar da visualização para interagir de uma maneira com o controlador de visualização, a maneira recomendada e o que a Apple faz no Cocoa é usar o padrão de delegação.

Um exemplo de como implementá-lo é o seguinte:

@protocol MyViewDelegate < NSObject >

- (void)viewActionHappened;

@end

@interface MyView : UIView

@property (nonatomic, assign) MyViewDelegate delegate;

@end

@interface MyViewController < MyViewDelegate >

@end

A visualização faz interface com seu representante (como UITableView faz, por exemplo) e não se importa se está implementada no controlador de visualização ou em qualquer outra classe que você acaba usando.

Minha resposta original é a seguinte: Eu não recomendo isso, nem o restante das respostas em que é alcançado o acesso direto ao controlador de exibição

Não há uma maneira integrada de fazer isso. Enquanto você pode obter em torno dele, adicionando uma IBOutletno UIViewe ligando estes no Interface Builder, isso não é recomendado. A visualização não deve saber sobre o controlador de visualização. Em vez disso, você deve fazer o que o @Phil M sugere e criar um protocolo para ser usado como delegado.

pgb
fonte
26
Esse é um péssimo conselho. Você não deve referenciar um controlador de exibição de uma exibição
Philippe Leybaert 5/10/10
6
@MattDiPasquale: sim, é um design ruim.
Philippe Leybaert
23
@Phillipe Leybaert Estou curioso para saber sua opinião sobre o melhor design para um evento de clique em botão que deve invocar alguma ação no controlador, sem que a exibição contenha uma referência ao controlador. Obrigado
Jonathon Horsman
3
Os projetos de exemplo da @PhilippeLeybaert Apple tendem a demonstrar o uso de recursos específicos da API. Muitos, a quem me referi, sacrificam um design bom ou escalável para fornecer uma demonstração concisa do tópico. Levei muito tempo para perceber isso e, embora faça sentido, acho lamentável. Acho que muitos desenvolvedores tomam esses projetos pragmáticos como o guia da Apple para as melhores práticas de design, o que tenho certeza de que não.
Benjohn
11
Tudo isso sobre "você não deve fazer isso" é apenas isso. Se uma visão deseja saber sobre seu controlador de visão, cabe ao programador decidir. período.
Daniel Kanaan
203

Usando o exemplo postado por Brock, modifiquei-o para que seja uma categoria de UIView, em vez de UIViewController, e o tornei recursivo para que qualquer subvisão possa (espero) encontrar o UIViewController pai.

@interface UIView (FindUIViewController)
- (UIViewController *) firstAvailableUIViewController;
- (id) traverseResponderChainForUIViewController;
@end

@implementation UIView (FindUIViewController)
- (UIViewController *) firstAvailableUIViewController {
    // convenience function for casting and to "mask" the recursive function
    return (UIViewController *)[self traverseResponderChainForUIViewController];
}

- (id) traverseResponderChainForUIViewController {
    id nextResponder = [self nextResponder];
    if ([nextResponder isKindOfClass:[UIViewController class]]) {
        return nextResponder;
    } else if ([nextResponder isKindOfClass:[UIView class]]) {
        return [nextResponder traverseResponderChainForUIViewController];
    } else {
        return nil;
    }
}
@end

Para usar esse código, adicione-o a um novo arquivo de classe (nomeiei meu "UIKitCategories") e remova os dados da classe ... copie a interface @ no cabeçalho e a implementação @ no arquivo .m. Em seu projeto, #import "UIKitCategories.h" e use no código UIView:

// from a UIView subclass... returns nil if UIViewController not available
UIViewController * myController = [self firstAvailableUIViewController];
Phil M
fonte
47
E uma razão pela qual você precisa permitir que o UIView esteja ciente de seu UIViewController é quando você tem subclasses personalizadas do UIView que precisam enviar uma exibição / caixa de diálogo modal.
Phil M
Impressionante, eu tive que acessar meu ViewController para exibir um pop-up personalizado que é criado por uma
sub
2
Não é uma prática ruim para um UIView promover uma exibição modal? Estou fazendo isso agora, mas eu sinto que não é a coisa certa a fazer ..
Van Du Tran
6
Phil, sua visualização personalizada deve chamar um método delegado que o controlador de visualização ouve e depois envia a partir daí.
malhal
9
eu simplesmente amo quantas perguntas do SO tem uma resposta "acadêmica", acadêmica, aceita com apenas alguns pontos e uma segunda resposta prática, suja e contra as regras, com dez vezes os pontos :-)
hariseldon78
114

UIViewé uma subclasse de UIResponder. UIResponderestabelece o método -nextRespondercom uma implementação que retorna nil. UIViewsubstitui esse método, conforme documentado em UIResponder(por algum motivo, em vez de em UIView) da seguinte maneira: se a visualização tiver um controlador de visualização, ele será retornado por -nextResponder. Se não houver um controlador de exibição, o método retornará a superview.

Adicione isso ao seu projeto e você estará pronto para começar.

@interface UIView (APIFix)
- (UIViewController *)viewController;
@end

@implementation UIView (APIFix)

- (UIViewController *)viewController {
    if ([self.nextResponder isKindOfClass:UIViewController.class])
        return (UIViewController *)self.nextResponder;
    else
        return nil;
}
@end

Agora UIViewtem um método de trabalho para retornar o controlador de exibição.

Brock
fonte
5
Isso funcionará apenas se não houver nada na cadeia de resposta entre o receptor UIViewe o UIViewController. A resposta de Phil M com recursão é o caminho a percorrer.
217 Olivier
33

Eu sugeriria uma abordagem mais leve para percorrer toda a cadeia de respostas sem precisar adicionar uma categoria no UIView:

@implementation MyUIViewSubclass

- (UIViewController *)viewController {
    UIResponder *responder = self;
    while (![responder isKindOfClass:[UIViewController class]]) {
        responder = [responder nextResponder];
        if (nil == responder) {
            break;
        }
    }
    return (UIViewController *)responder;
}

@end
de.
fonte
22

Combinando várias respostas já fornecidas, eu também as envio com a minha implementação:

@implementation UIView (AppNameAdditions)

- (UIViewController *)appName_viewController {
    /// Finds the view's view controller.

    // Take the view controller class object here and avoid sending the same message iteratively unnecessarily.
    Class vcc = [UIViewController class];

    // Traverse responder chain. Return first found view controller, which will be the view's view controller.
    UIResponder *responder = self;
    while ((responder = [responder nextResponder]))
        if ([responder isKindOfClass: vcc])
            return (UIViewController *)responder;

    // If the view controller isn't found, return nil.
    return nil;
}

@end

A categoria faz parte da minha biblioteca estática habilitada para ARC que eu envio em todos os aplicativos que eu criar. Foi testado várias vezes e não encontrei nenhum problema ou vazamento.

PS: Você não precisa usar uma categoria como eu fiz se a exibição em questão for uma subclasse sua. Neste último caso, basta colocar o método na sua subclasse e você estará pronto.

Constantino Tsarouhas
fonte
1
Esta é a melhor resposta. Não há necessidade de recursão, esta versão é bem otimizado
zeroimpl
12

Mesmo que isso possa ser tecnicamente resolvido como recomenda a pgb , IMHO, essa é uma falha de design. A visualização não precisa estar ciente do controlador.

Ushox
fonte
Só não tenho certeza de como o viewController pode ser informado de que uma de suas visualizações está desaparecendo e precisa chamar um dos métodos viewXXXAppear / viewXXXDisappear.
mahboudz 03/09/09
2
Essa é a ideia por trás do padrão Observer. O Observado (a Visão neste caso) não deve estar ciente de seus Observadores diretamente. The Observer só deve receber os retornos de chamada que está interessado.
Ushox
12

I modificado de resposta para que eu possa passar qualquer ponto de vista, botão, etiqueta etc. para obtê-lo de pai UIViewController. Aqui está o meu código.

+(UIViewController *)viewController:(id)view {
    UIResponder *responder = view;
    while (![responder isKindOfClass:[UIViewController class]]) {
        responder = [responder nextResponder];
        if (nil == responder) {
            break;
        }
    }
    return (UIViewController *)responder;
}

Editar versão do Swift 3

class func viewController(_ view: UIView) -> UIViewController {
        var responder: UIResponder? = view
        while !(responder is UIViewController) {
            responder = responder?.next
            if nil == responder {
                break
            }
        }
        return (responder as? UIViewController)!
    }

Edição 2: - Extensão rápida

extension UIView
{
    //Get Parent View Controller from any view
    func parentViewController() -> UIViewController {
        var responder: UIResponder? = self
        while !(responder is UIViewController) {
            responder = responder?.next
            if nil == responder {
                break
            }
        }
        return (responder as? UIViewController)!
    }
}
Varun Naharia
fonte
7

Não esqueça que você pode obter acesso ao controlador de visualização raiz da janela da qual a visualização é uma subvisão. A partir daí, se você estiver, por exemplo, usando um controlador de visualização de navegação e desejar inserir uma nova visualização nele:

    [[[[self window] rootViewController] navigationController] pushViewController:newController animated:YES];

Você precisará configurar corretamente a propriedade rootViewController da janela primeiro. Faça isso quando criar o controlador, por exemplo, no delegado do aplicativo:

-(void) applicationDidFinishLaunching:(UIApplication *)application {
    window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    RootViewController *controller = [[YourRootViewController] alloc] init];
    [window setRootViewController: controller];
    navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController];
    [controller release];
    [window addSubview:[[self navigationController] view]];
    [window makeKeyAndVisible];
}
Gabriel
fonte
Parece-me que, de acordo com os documentos da Apple, uma vez que [[self navigationController] view]é a (sub) visualização "principal" da janela, a rootViewControllerpropriedade da janela deve ser configurada para navigationControllercontrolar a visualização "principal" imediatamente.
adubr 14/05
6

Embora essas respostas sejam tecnicamente corretas, incluindo a Ushox, acho que a maneira aprovada é implementar um novo protocolo ou reutilizar um já existente. Um protocolo isola o observador do observado, como colocar um slot de correio entre eles. Com efeito, é isso que Gabriel faz através da invocação do método pushViewController; a view "sabe" que é um protocolo adequado solicitar educadamente ao seu navigationController que forneça uma visualização, pois o viewController está em conformidade com o protocolo navigationController. Embora você possa criar seu próprio protocolo, basta usar o exemplo de Gabriel e reutilizar o protocolo UINavigationController.

PapaSmurf
fonte
6

Eu me deparei com uma situação em que tenho um pequeno componente que quero reutilizar e adicionei algum código em uma exibição reutilizável (na verdade, não é muito mais do que um botão que abre a PopoverController).

Enquanto isso funciona bem no iPad (o que se UIPopoverControllerapresenta, portanto, não precisa de referência a UIViewController), fazer com que o mesmo código funcione significa fazer uma referência repentina presentViewControllerà sua UIViewController. Meio inconsistente, certo?

Como mencionado anteriormente, não é a melhor abordagem para ter lógica no seu UIView. Mas parecia realmente inútil agrupar as poucas linhas de código necessárias em um controlador separado.

De qualquer maneira, aqui está uma solução rápida, que adiciona uma nova propriedade a qualquer UIView:

extension UIView {

    var viewController: UIViewController? {

        var responder: UIResponder? = self

        while responder != nil {

            if let responder = responder as? UIViewController {
                return responder
            }
            responder = responder?.nextResponder()
        }
        return nil
    }
}
Kevin R
fonte
5

Não acho que seja "péssima" descobrir quem é o controlador de exibição em alguns casos. O que poderia ser uma má idéia é salvar a referência a este controlador, pois ele pode mudar da mesma forma que as superviews. No meu caso, tenho um getter que atravessa a cadeia de respostas.

//.h

@property (nonatomic, readonly) UIViewController * viewController;

//.m

- (UIViewController *)viewController
{
    for (UIResponder * nextResponder = self.nextResponder;
         nextResponder;
         nextResponder = nextResponder.nextResponder)
    {
        if ([nextResponder isKindOfClass:[UIViewController class]])
            return (UIViewController *)nextResponder;
    }

    // Not found
    NSLog(@"%@ doesn't seem to have a viewController". self);
    return nil;
}
Rivera
fonte
4

O loop do while mais simples para encontrar o viewController.

-(UIViewController*)viewController
{
    UIResponder *nextResponder =  self;

    do
    {
        nextResponder = [nextResponder nextResponder];

        if ([nextResponder isKindOfClass:[UIViewController class]])
            return (UIViewController*)nextResponder;

    } while (nextResponder != nil);

    return nil;
}
Mohd Iftekhar Qurashi
fonte
4

Swift 4

(mais conciso que as outras respostas)

fileprivate extension UIView {

  var firstViewController: UIViewController? {
    let firstViewController = sequence(first: self, next: { $0.next }).first(where: { $0 is UIViewController })
    return firstViewController as? UIViewController
  }

}

Meu caso de uso para que eu preciso para acessar a exibição primeiro UIViewController: Eu tenho um objeto que envolve em torno de AVPlayer/ AVPlayerViewControllere eu quero fornecer um simples show(in view: UIView)método que irá incorporar AVPlayerViewControllerem view. Para isso, eu preciso de acesso viewé UIViewController.

frouo
fonte
3

Isso não responde diretamente à pergunta, mas faz uma suposição sobre a intenção da pergunta.

Se você tem uma visão e, nessa visão, precisa chamar um método em outro objeto, como o controlador de visão, é possível usar o NSNotificationCenter.

Primeiro, crie sua sequência de notificações em um arquivo de cabeçalho

#define SLCopyStringNotification @"ShaoloCopyStringNotification"

Na sua opinião, ligue para postNotificationName:

- (IBAction) copyString:(id)sender
{
    [[NSNotificationCenter defaultCenter] postNotificationName:SLCopyStringNotification object:nil];
}

Então, no seu controlador de exibição, você adiciona um observador. Eu faço isso em viewDidLoad

- (void)viewDidLoad
{
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(copyString:)
                                                 name:SLCopyStringNotification
                                               object:nil];
}

Agora (também no mesmo controlador de exibição) implemente seu método copyString: como representado no @selector acima.

- (IBAction) copyString:(id)sender
{
    CalculatorResult* result = (CalculatorResult*)[[PercentCalculator sharedInstance].arrayTableDS objectAtIndex:([self.viewTableResults indexPathForSelectedRow].row)];
    UIPasteboard *gpBoard = [UIPasteboard generalPasteboard];
    [gpBoard setString:result.stringResult];
}

Não estou dizendo que essa é a maneira certa de fazer isso, apenas parece mais limpo do que executar a cadeia de resposta inicial. Usei esse código para implementar um UIMenuController em um UITableView e passar o evento de volta ao UIViewController para que eu possa fazer algo com os dados.

Shaolo
fonte
3

Certamente é uma má idéia e um design errado, mas tenho certeza de que todos podemos desfrutar de uma solução Swift da melhor resposta proposta por @Phil_M:

static func firstAvailableUIViewController(fromResponder responder: UIResponder) -> UIViewController? {
    func traverseResponderChainForUIViewController(responder: UIResponder) -> UIViewController? {
        if let nextResponder = responder.nextResponder() {
            if let nextResp = nextResponder as? UIViewController {
                return nextResp
            } else {
                return traverseResponderChainForUIViewController(nextResponder)
            }
        }
        return nil
    }

    return traverseResponderChainForUIViewController(responder)
}

Se sua intenção é fazer coisas simples, como mostrar um diálogo modal ou dados de rastreamento, isso não justifica o uso de um protocolo. Eu pessoalmente guardo essa função em um objeto utilitário, você pode usá-la de qualquer coisa que implemente o protocolo UIResponder como:

if let viewController = MyUtilityClass.firstAvailableUIViewController(self) {}

Todo o crédito a @Phil_M

Paul Slm
fonte
3

Talvez eu esteja atrasado aqui. Mas nesta situação eu não gosto de categoria (poluição). Eu amo assim:

#define UIViewParentController(__view) ({ \
UIResponder *__responder = __view; \
while ([__responder isKindOfClass:[UIView class]]) \
__responder = [__responder nextResponder]; \
(UIViewController *)__responder; \
})
lee
fonte
3

Solução mais rápida

extension UIView {
    var parentViewController: UIViewController? {
        for responder in sequence(first: self, next: { $0.next }) {
            if let viewController = responder as? UIViewController {
                return viewController
            }
        }
        return nil
    }
}
Igor Palaguta
fonte
Eu gosto desta resposta. Não tenho certeza se é mais rápido. Swift é mais um idioma que não pode decidir qual dos múltiplos paradigmas que deseja casar com as línguas. Nesse caso, você tem uma boa demonstração de uma abordagem mais funcional (o sequencebit). Então, se "swifty" significa "mais funcional", acho que é mais swifty.
Travis Griggs
2

Versão atualizada para o swift 4: Obrigado por @Phil_M e @ paul-slm

static func firstAvailableUIViewController(fromResponder responder: UIResponder) -> UIViewController? {
    func traverseResponderChainForUIViewController(responder: UIResponder) -> UIViewController? {
        if let nextResponder = responder.next {
            if let nextResp = nextResponder as? UIViewController {
                return nextResp
            } else {
                return traverseResponderChainForUIViewController(responder: nextResponder)
            }
        }
        return nil
    }

    return traverseResponderChainForUIViewController(responder: responder)
}
dicle
fonte
2

Versão Swift 4

extension UIView {
var parentViewController: UIViewController? {
    var parentResponder: UIResponder? = self
    while parentResponder != nil {
        parentResponder = parentResponder!.next
        if let viewController = parentResponder as? UIViewController {
            return viewController
        }
    }
    return nil
}

Exemplo de uso

 if let parent = self.view.parentViewController{

 }
levin varghese
fonte
2

Duas soluções a partir do Swift 5.2 :

  • Mais sobre o lado funcional
  • Não há necessidade da returnpalavra-chave agora 🤓

Solução 1:

extension UIView {
    var parentViewController: UIViewController? {
        sequence(first: self) { $0.next }
            .first(where: { $0 is UIViewController })
            .flatMap { $0 as? UIViewController }
    }
}

Solução 2:

extension UIView {
    var parentViewController: UIViewController? {
        sequence(first: self) { $0.next }
            .compactMap{ $0 as? UIViewController }
            .first
    }
}
  • Esta solução requer iteração através de cada respondedor primeiro, portanto, pode não ser o melhor desempenho.
jason z
fonte
1

Para a resposta de Phil:

Na linha: id nextResponder = [self nextResponder]; se self (UIView) não é uma subvisualização da exibição do ViewController, se você conhece a hierarquia de self (UIView), também pode usar: id nextResponder = [[self superview] nextResponder];...

ceder
fonte
0

Minha solução provavelmente seria considerada meio falsa, mas eu tive uma situação semelhante a mayoneez (eu queria trocar de vista em resposta a um gesto em um EAGLView) e obtive o controlador de exibição do EAGL da seguinte maneira:

EAGLViewController *vc = ((EAGLAppDelegate*)[[UIApplication sharedApplication] delegate]).viewController;
gulchrider
fonte
2
Por favor, reescrever seu código da seguinte forma: EAGLViewController *vc = [(EAGLAppDelegate *)[UIApplication sharedApplication].delegate viewController];.
Jonathan Sterling
1
O problema não está na sintaxe do ponto, mas nos tipos. No Objetivo C, para declarar um objeto que você escreve ClassName *object- com um asterisco.
adubr 14/05
Opa ... isso é realmente o que eu tinha, mas o widget HTML do StackOverflow parece que achou que o asterisco significava itálico ... Eu mudei para um bloco de código, agora ele é exibido corretamente. Obrigado!
Gulchrider 7/08
0

Eu acho que há um caso em que o observado precisa informar o observador.

Vejo um problema semelhante em que o UIView em um UIViewController está respondendo a uma situação e ele precisa primeiro informar ao controlador de exibição pai para ocultar o botão voltar e, após a conclusão, informar ao controlador de exibição pai que ele precisa sair da pilha.

Eu tenho tentado isso com delegados sem sucesso.

Eu não entendo por que isso deve ser uma má idéia?

user7865437
fonte
0

Outra maneira fácil é ter sua própria classe de exibição e adicionar uma propriedade do controlador de exibição na classe de exibição. Geralmente, o controlador de visualização cria a visualização e é aí que o controlador pode se definir para a propriedade. Basicamente, é em vez de procurar ao redor (com um pouco de hacking) pelo controlador, fazendo com que o controlador se ajuste à visualização - isso é simples, mas faz sentido, porque é o controlador que "controla" a visualização.

jack
fonte
0

Se você não fizer o upload para a App Store, também poderá usar um método privado do UIView.

@interface UIView(Private)
- (UIViewController *)_viewControllerForAncestor;
@end

// Later in the code
UIViewController *vc = [myView _viewControllerForAncestor];

fonte
0
var parentViewController: UIViewController? {
    let s = sequence(first: self) { $0.next }
    return s.compactMap { $0 as? UIViewController }.first
}
klaudas
fonte
Embora esse código possa responder à pergunta, uma boa resposta também deve explicar o que o código faz e como ele resolve o problema.
BDL
0

Para obter o controlador de uma determinada exibição, pode-se usar a cadeia UIFirstResponder.

customView.target(forAction: Selector("viewDidLoad"), withSender: nil)
Adobels
fonte
-1

Se o seu rootViewController for UINavigationViewController, que foi configurado na classe AppDelegate,

    + (UIViewController *) getNearestViewController:(Class) c {
NSArray *arrVc = [[[[UIApplication sharedApplication] keyWindow] rootViewController] childViewControllers];

for (UIViewController *v in arrVc)
{
    if ([v isKindOfClass:c])
    {
        return v;
    }
}

return nil;}

Onde c é necessário, veja a classe de controladores.

USO:

     RequiredViewController* rvc = [Utilities getNearestViewController:[RequiredViewController class]];
Ansari Awais
fonte
-5

Não tem jeito.

O que faço é passar o ponteiro UIViewController para o UIView (ou uma herança apropriada). Me desculpe, eu não posso ajudar com a abordagem da IB para o problema, porque eu não acredito em IB.

Para responder ao primeiro comentarista: às vezes você precisa saber quem ligou para você, porque determina o que você pode fazer. Por exemplo, com um banco de dados, você pode ter somente acesso de leitura ou leitura / gravação ...

John Smith
fonte
10
O que isso significa - "eu não acredito em IB"? Eu o lancei, ele certamente existe.
Marcc
5
Você precisa entender melhor a diversão e a abstração, principalmente no que diz respeito ao idioma inglês. Isso significa que eu não gosto disso.
John Smith
4
Eu não gosto de abacates. Mas aposto que posso ajudar alguém a fazer guacamole. Isso pode ser feito no IB, portanto, obviamente, sua resposta de "não há como" está incorreta. Se você gosta de IB ou não, é irrelevante.
Feloneous Cat