¿Llegar a UIViewController desde UIView?


Respuestas:


46

Como esta ha sido la respuesta aceptada durante mucho tiempo, siento que necesito rectificarla con una respuesta mejor.

Algunos comentarios sobre la necesidad:

  • Su vista no debería necesitar acceder al controlador de vista directamente.
  • En cambio, la vista debe ser independiente del controlador de vista y poder trabajar en diferentes contextos.
  • Si necesita que la vista interactúe de una manera con el controlador de vista, la forma recomendada y lo que Apple hace en Cocoa es usar el patrón de delegado.

A continuación se muestra un ejemplo de cómo implementarlo:

@protocol MyViewDelegate < NSObject >

- (void)viewActionHappened;

@end

@interface MyView : UIView

@property (nonatomic, assign) MyViewDelegate delegate;

@end

@interface MyViewController < MyViewDelegate >

@end

La vista interactúa con su delegado (como lo UITableViewhace, por ejemplo) y no le importa si se implementa en el controlador de vista o en cualquier otra clase que termine usando.

Mi respuesta original sigue: no recomiendo esto, ni el resto de las respuestas donde se logra el acceso directo al controlador de vista

No hay una forma integrada de hacerlo. Mientras que usted puede conseguir alrededor de él mediante la adición de una IBOutletsobre la UIViewy la conexión de estos en Interface Builder, esto no es recomendable. La vista no debe saber sobre el controlador de vista. En su lugar, debe hacer lo que @Phil M sugiere y crear un protocolo para usar como delegado.


26
Eso es muy mal consejo. No debe hacer referencia a un controlador de vista desde una vista
Philippe Leybaert

66
@MattDiPasquale: sí, es un mal diseño.
Philippe Leybaert

23
@Phillipe Leybaert Tengo curiosidad por conocer su opinión sobre el mejor diseño para un evento de clic de botón que debería invocar alguna acción en el controlador, sin que la vista tenga una referencia al controlador. Gracias
Jonathon Horsman

3
@PhilippeLeybaert Los proyectos de ejemplo de Apple tienden a demostrar el uso de características API particulares. Muchos me he referido a sacrificar un diseño bueno o escalable para proporcionar una demostración concisa del tema. Me tomó mucho tiempo darme cuenta de esto y, aunque tiene sentido, me parece desafortunado. Creo que muchos desarrolladores toman estos proyectos pragmáticos como la guía de Apple para las mejores prácticas de diseño, lo cual estoy bastante seguro de que no lo son.
Benjohn

11
Todo esto sobre "no deberías hacer esto" es solo eso. Si una vista quiere saber sobre su controlador de vista, eso depende del programador. período.
Daniel Kanaan

203

Usando el ejemplo publicado por Brock, lo modifiqué para que sea una categoría de UIView en lugar de UIViewController y lo hice recursivo para que cualquier subvista pueda (con suerte) encontrar el UIViewController padre.

@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 este código, agréguelo a un nuevo archivo de clase (llamé al mío "UIKitCategories") y elimine los datos de la clase ... copie la @interface en el encabezado y la @implementation en el archivo .m. Luego, en su proyecto, # importe "UIKitCategories.h" y use dentro del código UIView:

// from a UIView subclass... returns nil if UIViewController not available
UIViewController * myController = [self firstAvailableUIViewController];

47
Y una razón por la que debe permitir que UIView conozca su UIViewController es cuando tiene subclases de UIView personalizadas que necesitan presionar una vista / diálogo modal.
Phil M

Impresionante, tuve que acceder a mi ViewController para mostrar una ventana emergente personalizada creada por una subvista
aryaxt

2
¿No es una mala práctica para una UIView impulsar una vista modal? Estoy haciendo esto ahora pero siento que no es lo correcto ...
Van Du Tran

66
Phil, tu vista personalizada debería llamar a un método de delegado que el controlador de vista escucha y luego empuja desde allí.
malhal

9
Me encantan cuántas preguntas SO tiene un "sabio", académico, respuesta aceptada con solo unos pocos puntos y una segunda respuesta que es práctica, sucia y en contra de las reglas, con diez veces los puntos :-)
hariseldon78

114

UIViewes una subclase de UIResponder. UIResponderpresenta el método -nextRespondercon una implementación que devuelve nil. UIViewanula este método, como se documenta en UIResponder(por alguna razón en lugar de en UIView) de la siguiente manera: si la vista tiene un controlador de vista, es devuelta por -nextResponder. Si no hay un controlador de vista, el método devolverá la supervista.

Agregue esto a su proyecto y estará listo para comenzar.

@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

Ahora UIViewtiene un método de trabajo para devolver el controlador de vista.


55
Esto solo funcionará si no hay nada en la cadena de respuesta entre el receptor UIViewy el UIViewController. La respuesta de Phil M con recursión es el camino a seguir.
Olivier

33

Sugeriría un enfoque más liviano para atravesar la cadena de respuesta completa sin tener que agregar una categoría en UIView:

@implementation MyUIViewSubclass

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

@end

22

Combinando varias respuestas ya dadas, también lo envío con mi implementación:

@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

La categoría es parte de mi biblioteca estática habilitada para ARC que envío en cada aplicación que creo. Ha sido probado varias veces y no encontré ningún problema o fuga.

PD: No necesita usar una categoría como lo hice si la vista en cuestión es una subclase suya. En el último caso, simplemente coloque el método en su subclase y estará listo.


1
Esta es la mejor respuesta. No hay necesidad de recurrencia, esta versión está muy bien optimizada
zeroimpl

12

Aunque técnicamente esto se puede resolver como lo recomienda pgb , en mi humilde opinión, este es un defecto de diseño. La vista no debería necesitar ser consciente del controlador.


No estoy seguro de cómo se puede decir a viewController que una de sus vistas desaparecerá y necesita llamar a uno de sus métodos viewXXXAppear / viewXXXDisappear.
mahboudz

2
Esta es la idea detrás del patrón Observador. El Observado (la Vista en este caso) no debe conocer a sus Observadores directamente. El observador sólo debe recibir las devoluciones de llamada que le interesa.
Ushox

12

He modificado de respuesta, así que puede pasar cualquier punto de vista, botón, etiqueta, etc. conseguirlo de los padres UIViewController. Aquí está mi 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 versión de 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)!
    }

Edición 2: - Extensión 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)!
    }
}

7

No olvide que puede obtener acceso al controlador de vista raíz para la ventana de la que la vista es una subvista. A partir de ahí, si está utilizando, por ejemplo, un controlador de vista de navegación y desea insertar una nueva vista:

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

Sin embargo, primero deberá configurar correctamente la propiedad rootViewController de la ventana. Haga esto cuando cree el controlador por primera vez, por ejemplo, en el delegado de su aplicación:

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

Me parece que, de acuerdo con los documentos de Apple, dado que [[self navigationController] view]es la vista (principal) "principal" de la ventana, la rootViewControllerpropiedad de la ventana debe configurarse para navigationControllerque controle la vista "principal" de inmediato.
adubr

6

Si bien estas respuestas son técnicamente correctas, incluido Ushox, creo que la forma aprobada es implementar un nuevo protocolo o reutilizar uno existente. Un protocolo aísla al observador de lo observado, algo así como poner una ranura de correo entre ellos. En efecto, eso es lo que Gabriel hace a través de la invocación del método pushViewController; la vista "sabe" que es un protocolo apropiado pedir educadamente a su navigationController que empuje una vista, ya que viewController se ajusta al protocolo navigationController. Si bien puede crear su propio protocolo, simplemente usando el ejemplo de Gabriel y reutilizando el protocolo UINavigationController está bien.


6

Me topé con una situación en la que tengo un pequeño componente que quiero reutilizar, y agregué un código en una vista reutilizable (en realidad no es mucho más que un botón que abre un PopoverController).

Si bien esto funciona bien en el iPad (el UIPopoverControllerpresente en sí mismo, por lo tanto, no necesita hacer referencia a a UIViewController), hacer que el mismo código funcione significa que de repente haga referencia presentViewControllera su UIViewController. Un poco inconsistente ¿verdad?

Como se mencionó anteriormente, no es el mejor enfoque tener lógica en su UIView. Pero se sintió realmente inútil envolver las pocas líneas de código necesarias en un controlador separado.

De cualquier manera, aquí hay una solución rápida, que agrega una nueva propiedad a cualquier 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
    }
}

5

No creo que sea "mala" idea averiguar quién es el controlador de la vista en algunos casos. Lo que podría ser una mala idea es guardar la referencia a este controlador, ya que podría cambiar al igual que cambian las supervistas. En mi caso, tengo un captador que atraviesa la cadena de respuesta.

//.h

@property (nonatomic, readonly) UIViewController * viewController;

//.metro

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

4

El bucle while más simple para encontrar viewController.

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

    do
    {
        nextResponder = [nextResponder nextResponder];

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

    } while (nextResponder != nil);

    return nil;
}

4

Swift 4

(más conciso que las otras respuestas)

fileprivate extension UIView {

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

}

Mi caso de uso para el que tenga que acceder a la vista primero UIViewController: Tengo un objeto que se envuelve alrededor AVPlayer/ AVPlayerViewControllery quiero dar un simple show(in view: UIView)método que va a incrustar AVPlayerViewControlleren view. Para eso, necesito acceder a view's UIViewController.


3

Esto no responde la pregunta directamente, sino que supone una suposición sobre la intención de la pregunta.

Si tiene una vista y en esa vista necesita llamar a un método en otro objeto, como por ejemplo el controlador de vista, puede usar el NSNotificationCenter en su lugar.

Primero cree su cadena de notificación en un archivo de encabezado

#define SLCopyStringNotification @"ShaoloCopyStringNotification"

En su opinión, llame a postNotificationName:

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

Luego, en su controlador de vista, agrega un observador. Hago esto en viewDidLoad

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

Ahora (también en el mismo controlador de vista) implemente su método copyString: como se muestra en el @selector anterior.

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

No digo que esta sea la forma correcta de hacer esto, simplemente parece más limpio que ejecutar la primera cadena de respuesta. Utilicé este código para implementar un UIMenuController en un UITableView y pasar el evento nuevamente al UIViewController para poder hacer algo con los datos.


3

Seguramente es una mala idea y un diseño incorrecto, pero estoy seguro de que todos podemos disfrutar de una solución Swift de la mejor respuesta propuesta 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)
}

Si su intención es hacer cosas simples, como mostrar un diálogo modal o datos de seguimiento, eso no justifica el uso de un protocolo. Personalmente almaceno esta función en un objeto de utilidad, puede usarla desde cualquier cosa que implemente el protocolo UIResponder como:

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

Todo el crédito a @Phil_M


3

Tal vez llego tarde aquí. Pero en esta situación no me gusta la categoría (contaminación). Me encanta de esta manera:

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

3

Solución más 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
    }
}

Me gusta esta respuesta Sin embargo, no estoy seguro de que sea más rápido. Swift es otro de los idiomas que no puede decidir cuál de los múltiples paradigmas quiere casarse. En este caso, tiene una buena demostración de un enfoque más funcional (el sequencebit). Entonces, si "rápido" significa "más funcional", entonces supongo que es más rápido.
Travis Griggs

2

Versión actualizada para swift 4: Gracias por @Phil_M y @ 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)
}

2

Versión 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
}

Ejemplo de uso

 if let parent = self.view.parentViewController{

 }

2

Dos soluciones a partir de Swift 5.2 :

  • Más en el lado funcional
  • No hay necesidad de la returnpalabra clave ahora 🤓

Solución 1:

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

Solución 2:

extension UIView {
    var parentViewController: UIViewController? {
        sequence(first: self) { $0.next }
            .compactMap{ $0 as? UIViewController }
            .first
    }
}
  • Esta solución requiere iterar primero a través de cada respondedor, por lo que puede no ser el más eficiente.

1

A la respuesta de Phil:

En línea: id nextResponder = [self nextResponder]; si self (UIView) no es una subvista de la vista ViewController, si conoce la jerarquía de self (UIView) puede usar también: id nextResponder = [[self superview] nextResponder];...


0

Mi solución probablemente se consideraría un poco falsa, pero tuve una situación similar a la de mayoneez (quería cambiar de vista en respuesta a un gesto en un EAGLView), y obtuve el controlador de vista del EAGL de esta manera:

EAGLViewController *vc = ((EAGLAppDelegate*)[[UIApplication sharedApplication] delegate]).viewController;

2
Por favor, vuelva a escribir el código de la siguiente manera: EAGLViewController *vc = [(EAGLAppDelegate *)[UIApplication sharedApplication].delegate viewController];.
Jonathan Sterling el

1
El problema no es con la sintaxis de puntos, sino con los tipos. En el objetivo C para declarar un objeto que escriba ClassName *object, con un asterisco.
adubr

Vaya ... eso es en realidad lo que tenía, pero el widget de HTML StackOverflow parece que pensó que el asterisco significaba cursiva ... Lo cambié a un bloque de código, ahora se muestra correctamente. ¡Gracias!
gulchrider

0

Creo que hay un caso en que lo observado necesita informar al observador.

Veo un problema similar en el que el UIView en un UIViewController está respondiendo a una situación y necesita primero decirle a su controlador de vista principal que oculte el botón Atrás y luego, al finalizar, decirle al controlador de vista principal que necesita salir de la pila.

He estado intentando esto con delegados sin éxito.

No entiendo por qué esto debería ser una mala idea?


0

Otra forma fácil es tener su propia clase de vista y agregar una propiedad del controlador de vista en la clase de vista. Por lo general, el controlador de vista crea la vista y ahí es donde el controlador puede establecerse en la propiedad. Básicamente, es en lugar de buscar (con un poco de pirateo) el controlador, tener el controlador configurado para la vista; esto es simple pero tiene sentido porque es el controlador el que "controla" la vista.


0

Si no va a subir esto a la App Store, también puede usar un método privado de UIView.

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

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

0
var parentViewController: UIViewController? {
    let s = sequence(first: self) { $0.next }
    return s.compactMap { $0 as? UIViewController }.first
}

Aunque este código podría responder la pregunta, una buena respuesta también debería explicar qué hace el código y cómo resuelve el problema.
BDL

0

Para obtener el controlador de una vista dada, uno puede usar la cadena UIFirstResponder.

customView.target(forAction: Selector("viewDidLoad"), withSender: nil)

-1

Si su rootViewController es UINavigationViewController, que se configuró en la clase AppDelegate, entonces

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

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

return nil;}

Donde c requiere ver la clase de controladores.

USO:

     RequiredViewController* rvc = [Utilities getNearestViewController:[RequiredViewController class]];

-5

No hay manera

Lo que hago es pasar el puntero UIViewController a UIView (o una herencia apropiada). Lo siento, no puedo ayudar con el enfoque del problema de IB porque no creo en IB.

Para responder al primer comentarista: a veces necesita saber quién lo llamó porque determina lo que puede hacer. Por ejemplo, con una base de datos, es posible que solo tenga acceso de lectura o lectura / escritura ...


10
¿Qué significa eso: "No creo en IB"? Lo lancé, ciertamente existe.
marcc

55
Necesita una mejor comprensión de la diversión y la abstracción, especialmente con respecto al idioma inglés. Significa que no me gusta.
John Smith

44
No me gustan los aguacates. Pero apuesto a que puedo ayudar a alguien a hacer guacamole. Se puede hacer en IB, por lo que obviamente su respuesta de "no hay manera" es incorrecta. Si te gusta el IB o no es irrelevante.
Feloneous Cat
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.