Accéder à UIViewController depuis UIView?


185

Existe-t-il un moyen intégré pour passer de a UIViewà son UIViewController? Je sais que vous pouvez passer de UIViewControllerà son UIViewvia [self view]mais je me demandais s'il y avait une référence inverse?

Réponses:


46

Puisque c'est la réponse acceptée depuis longtemps, je sens que je dois la rectifier avec une meilleure réponse.

Quelques commentaires sur le besoin:

  • Votre vue ne devrait pas avoir besoin d'accéder directement au contrôleur de vue.
  • La vue doit à la place être indépendante du contrôleur de vue et pouvoir fonctionner dans différents contextes.
  • Si vous avez besoin de la vue pour s'interfacer d'une manière avec le contrôleur de vue, la méthode recommandée et ce que fait Apple dans Cocoa est d'utiliser le modèle de délégué.

Voici un exemple de mise en œuvre:

@protocol MyViewDelegate < NSObject >

- (void)viewActionHappened;

@end

@interface MyView : UIView

@property (nonatomic, assign) MyViewDelegate delegate;

@end

@interface MyViewController < MyViewDelegate >

@end

La vue s'interface avec son délégué (comme UITableView fait, par exemple) et peu importe si elle est implémentée dans le contrôleur de vue ou dans toute autre classe que vous finissez par utiliser.

Ma réponse originale suit: Je ne recommande pas cela, ni le reste des réponses où un accès direct au contrôleur de vue est obtenu

Il n'y a pas de moyen intégré de le faire. Bien que vous puissiez le contourner en ajoutant un IBOutletsur UIViewet en les connectant dans Interface Builder, cela n'est pas recommandé. La vue ne doit pas connaître le contrôleur de vue. Au lieu de cela, vous devez faire comme @Phil M le suggère et créer un protocole à utiliser en tant que délégué.


26
C'est un très mauvais conseil. Vous ne devriez pas référencer un contrôleur de vue à partir d'une vue
Philippe Leybaert

6
@MattDiPasquale: oui, c'est une mauvaise conception.
Philippe Leybaert

23
@Phillipe Leybaert Je suis curieux de connaître votre avis sur la meilleure conception pour un événement de clic de bouton qui devrait invoquer une action sur le contrôleur, sans que la vue ne contienne une référence au contrôleur. Merci
Jonathon Horsman

3
@PhilippeLeybaert Les exemples de projets d'Apple ont tendance à démontrer l'utilisation de fonctionnalités d'API particulières. Beaucoup de ceux que j'ai mentionnés sacrifier une bonne conception ou une conception évolutive afin de fournir une démonstration concise du sujet. Il m'a fallu beaucoup de temps pour m'en rendre compte et bien que cela ait du sens, je trouve cela malheureux. Je pense que beaucoup de développeurs considèrent ces projets pragmatiques comme le guide d'Apple des meilleures pratiques de conception, ce que je suis convaincu qu'ils ne sont pas.
Benjohn

11
Tout ce bs sur "vous ne devriez pas faire ça" est juste cela. Si une vue veut connaître son contrôleur de vue, c'est au programmeur de décider. période.
Daniel Kanaan

203

En utilisant l'exemple posté par Brock, je l'ai modifié pour qu'il s'agisse d'une catégorie de UIView à la place de UIViewController et l'ai rendu récursif afin que toute sous-vue puisse (espérons-le) trouver le parent UIViewController.

@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

Pour utiliser ce code, ajoutez-le dans un nouveau fichier de classe (j'ai nommé le mien "UIKitCategories") et supprimez les données de classe ... copiez le @interface dans l'en-tête, et le @implementation dans le fichier .m. Ensuite, dans votre projet, #import "UIKitCategories.h" et utilisez dans le code UIView:

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

47
Et une des raisons pour lesquelles vous devez permettre à UIView de connaître son UIViewController est lorsque vous avez des sous-classes UIView personnalisées qui doivent pousser une vue / boîte de dialogue modale.
Phil M du

Génial, j'ai dû accéder à mon ViewController pour afficher une fenêtre contextuelle personnalisée qui est créée par une sous
vue

2
N'est-ce pas une mauvaise pratique pour un UIView de pousser une vue modale? Je fais ça en ce moment, mais je pense que ce n'est pas la bonne chose à faire ..
Van Du Tran

6
Phil, votre vue personnalisée doit appeler une méthode déléguée que le contrôleur de vue écoute, puis il pousse à partir de là.
malhal

9
j'aime juste combien de questions SO a un "homme sage", académique, réponse acceptée avec juste quelques points et une deuxième réponse qui est pratique, sale et contre les règles, avec dix fois les points :-)
hariseldon78

114

UIViewest une sous-classe de UIResponder. UIResponderprésente la méthode -nextResponderavec une implémentation qui retourne nil. UIViewremplace cette méthode, comme décrit dans UIResponder(pour une raison quelconque au lieu de in UIView) comme suit: si la vue a un contrôleur de vue, il est renvoyé par -nextResponder. S'il n'y a pas de contrôleur de vue, la méthode retournera le superview.

Ajoutez ceci à votre projet et vous êtes prêt à démarrer.

@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

A maintenant UIViewune méthode de travail pour renvoyer le contrôleur de vue.


5
Cela ne fonctionnera que s'il n'y a rien dans la chaîne de répondeurs entre le récepteur UIViewet le UIViewController. La réponse de Phil M avec la récursivité est la voie à suivre.
Olivier

33

Je suggérerais une approche plus légère pour parcourir la chaîne de répondeurs complète sans avoir à ajouter une catégorie sur 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

En combinant plusieurs réponses déjà données, j'expédie également dessus avec ma mise en œuvre:

@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 catégorie fait partie de ma fonctionnalité ARC bibliothèque statique que j'expédie sur chaque application que je crée. Il a été testé plusieurs fois et je n'ai trouvé ni problème ni fuite.

PS: Vous n'avez pas besoin d'utiliser une catégorie comme je l'ai fait si la vue concernée est une sous-classe de la vôtre. Dans ce dernier cas, mettez simplement la méthode dans votre sous-classe et vous êtes prêt à partir.


1
C'est la meilleure réponse. Pas besoin de récursion, cette version est joliment optimisée
zeroimpl

12

Même si cela peut techniquement être résolu comme le recommande pgb , à mon humble avis , il s'agit d'un défaut de conception. La vue ne devrait pas avoir besoin de connaître le contrôleur.


Je ne suis tout simplement pas sûr de savoir comment on peut dire au viewController que l'une de ses vues disparaît et qu'il doit appeler l'une de ses méthodes viewXXXAppear / viewXXXDisappear.
mahboudz

2
C'est l'idée derrière le modèle Observer. L'Observé (la Vue dans ce cas) ne doit pas être directement au courant de ses Observateurs. L'observateur ne devrait recevoir que les rappels qui l'intéressent.
Ushox

12

J'ai modifié de réponse afin que je puisse passer une vue, bouton, étiquette etc pour obtenir des parents est tout UIViewController. Voici mon code.

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

Modifier la version 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)!
    }

Edit 2: - Extension Swift

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

N'oubliez pas que vous pouvez accéder au contrôleur de vue racine pour la fenêtre dont la vue est une sous-vue. À partir de là, si vous utilisez par exemple un contrôleur de vue de navigation et que vous souhaitez y insérer une nouvelle vue:

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

Cependant, vous devrez d'abord configurer correctement la propriété rootViewController de la fenêtre. Faites ceci lorsque vous créez le contrôleur pour la première fois, par exemple dans votre délégué d'application:

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

Il me semble que conformément à la documentation Apple puisque [[self navigationController] view]c'est la (sous) vue "principale" de la fenêtre, la rootViewControllerpropriété de la fenêtre doit être définie sur navigationControllerlaquelle contrôle la vue "principale" immédiatement.
adubr

6

Bien que ces réponses soient techniquement correctes, y compris Ushox, je pense que la manière approuvée est de mettre en œuvre un nouveau protocole ou de réutiliser un protocole existant. Un protocole isole l'observateur de l'observé, un peu comme mettre une fente de courrier entre eux. En effet, c'est ce que fait Gabriel via l'invocation de la méthode pushViewController; la vue "sait" que c'est un protocole approprié pour demander poliment à votre navigationController de pousser une vue, puisque le viewController est conforme au protocole navigationController. Bien que vous puissiez créer votre propre protocole, il suffit d'utiliser l'exemple de Gabriel et de réutiliser le protocole UINavigationController.


6

Je suis tombé sur une situation où j'ai un petit composant que je veux réutiliser, et j'ai ajouté du code dans une vue réutilisable elle-même (ce n'est vraiment pas beaucoup plus qu'un bouton qui ouvre un PopoverController ).

Bien que cela fonctionne correctement dans l'iPad (le se UIPopoverControllerprésente, il n'est donc pas nécessaire de faire référence à a UIViewController), faire fonctionner le même code signifie soudainement référencer votre presentViewControllerdepuis votreUIViewController . Un peu incohérent, non?

Comme mentionné précédemment, ce n'est pas la meilleure approche pour avoir de la logique dans votre UIView. Mais il semblait vraiment inutile d'envelopper les quelques lignes de code nécessaires dans un contrôleur séparé.

Quoi qu'il en soit, voici une solution rapide, qui ajoute une nouvelle propriété à n'importe quel 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

Je ne pense pas que ce soit une "mauvaise" idée de savoir qui est le contrôleur de vue dans certains cas. Ce qui pourrait être une mauvaise idée est de sauvegarder la référence à ce contrôleur car elle pourrait changer tout comme les superviews changent. Dans mon cas, j'ai un getter qui traverse la chaîne de répondeurs.

//.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;
}

4

La boucle la plus simple pour trouver le 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

(plus concis que les autres réponses)

fileprivate extension UIView {

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

}

Mon cas d'utilisation pour lequel je dois d'abord accéder à la vue UIViewController: j'ai un objet qui s'enroule autour AVPlayer/ AVPlayerViewControlleret je souhaite fournir une show(in view: UIView)méthode simple qui s'intègre AVPlayerViewControllerdans view. Pour cela, j'ai besoin d'accéder viewaux fichiers UIViewController.


3

Cela ne répond pas directement à la question, mais fait plutôt une hypothèse sur l'intention de la question.

Si vous avez une vue et que dans cette vue, vous devez appeler une méthode sur un autre objet, comme par exemple le contrôleur de vue, vous pouvez utiliser le NSNotificationCenter à la place.

Créez d'abord votre chaîne de notification dans un fichier d'en-tête

#define SLCopyStringNotification @"ShaoloCopyStringNotification"

Dans votre vue, appelez postNotificationName:

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

Ensuite, dans votre contrôleur de vue, vous ajoutez un observateur. Je fais cela dans viewDidLoad

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

Maintenant (également dans le même contrôleur de vue) implémentez votre méthode copyString: comme illustré dans le @selector ci-dessus.

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

Je ne dis pas que c'est la bonne façon de faire cela, cela semble juste plus propre que de remonter la chaîne des premiers intervenants. J'ai utilisé ce code pour implémenter un UIMenuController sur un UITableView et transmettre l'événement à l'UIViewController afin que je puisse faire quelque chose avec les données.


3

C'est sûrement une mauvaise idée et une mauvaise conception, mais je suis sûr que nous pouvons tous profiter d'une solution Swift de la meilleure réponse proposée par @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 votre intention est de faire des choses simples, comme afficher une boîte de dialogue modale ou suivre des données, cela ne justifie pas l'utilisation d'un protocole. Personnellement, je stocke cette fonction dans un objet utilitaire, vous pouvez l'utiliser à partir de tout ce qui implémente le protocole UIResponder comme:

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

Tout le crédit à @Phil_M


3

Peut-être que je suis en retard ici. Mais dans cette situation je n'aime pas la catégorie (pollution). J'adore cette façon:

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

3

Solution plus rapide

extension UIView {
    var parentViewController: UIViewController? {
        for responder in sequence(first: self, next: { $0.next }) {
            if let viewController = responder as? UIViewController {
                return viewController
            }
        }
        return nil
    }
}

J'aime cette réponse. Je ne suis pas sûr que ce soit plus rapide. Swift est encore-un-autre-ne-peut-pas-décider-lequel-des-multiples-paradigmes-il-veut-se marier avec les langues. Dans ce cas, vous avez une belle démonstration d'une approche plus fonctionnelle (le sequencebit). Donc, si "swifty" signifie "plus fonctionnel", alors je suppose que c'est plus rapide.
Travis Griggs

2

Version mise à jour pour swift 4: merci pour @Phil_M et @ 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

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

Exemple d'utilisation

 if let parent = self.view.parentViewController{

 }

2

Deux solutions à partir de Swift 5.2 :

  • Plus sur le côté fonctionnel
  • Pas besoin de returnmot - clé maintenant 🤓

Solution 1:

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

Solution 2:

extension UIView {
    var parentViewController: UIViewController? {
        sequence(first: self) { $0.next }
            .compactMap{ $0 as? UIViewController }
            .first
    }
}
  • Cette solution nécessite d'abord une itération dans chaque répondeur, elle n'est donc peut-être pas la plus performante.

1

À la réponse de Phil:

En ligne: id nextResponder = [self nextResponder]; si self (UIView) n'est pas une sous-vue de la vue ViewController, si vous connaissez la hiérarchie de self (UIView), vous pouvez également utiliser: id nextResponder = [[self superview] nextResponder];...


0

Ma solution serait probablement considérée comme un peu bidon, mais j'ai eu une situation similaire à mayoneez (je voulais changer de vue en réponse à un geste dans un EAGLView), et j'ai obtenu le contrôleur de vue de l'EAGL de cette façon:

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

2
S'il vous plaît réécrire votre code comme suit: EAGLViewController *vc = [(EAGLAppDelegate *)[UIApplication sharedApplication].delegate viewController];.
Jonathan Sterling

1
Le problème n'est pas avec la syntaxe des points, mais avec les types. En Objective C pour déclarer un objet que vous écrivez ClassName *object- avec un astérisque.
adubr

Oups ... c'est en fait ce que j'avais, mais le widget HTML StackOverflow semble penser que l'astérisque signifiait italique ... Je l'ai changé en bloc de code, maintenant il s'affiche correctement. Merci!
gulchrider

0

Je pense qu'il y a un cas où l'observé doit informer l'observateur.

Je vois un problème similaire où l'UIView dans un UIViewController répond à une situation et il doit d'abord dire à son contrôleur de vue parent de masquer le bouton Précédent, puis, une fois terminé, dire au contrôleur de vue parent qu'il doit se sortir de la pile.

J'ai essayé cela avec des délégués sans succès.

Je ne comprends pas pourquoi cela devrait être une mauvaise idée?


0

Un autre moyen simple consiste à avoir votre propre classe de vue et à ajouter une propriété du contrôleur de vue dans la classe de vue. Habituellement, le contrôleur de vue crée la vue et c'est là que le contrôleur peut se définir sur la propriété. Fondamentalement, c'est au lieu de chercher (avec un peu de piratage) le contrôleur, d'avoir le contrôleur pour se définir sur la vue - c'est simple mais logique car c'est le contrôleur qui "contrôle" la vue.


0

Si vous n'allez pas le télécharger sur l'App Store, vous pouvez également utiliser une méthode privée d'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
}

Bien que ce code puisse répondre à la question, une bonne réponse devrait également expliquer ce que fait le code et comment il résout le problème.
BDL

0

Pour obtenir le contrôleur d'une vue donnée, on peut utiliser la chaîne UIFirstResponder.

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

-1

Si votre rootViewController est UINavigationViewController, qui a été configuré dans la classe AppDelegate, alors

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

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

return nil;}

Où c requis la classe de contrôleurs de vue.

USAGE:

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

-5

Il n'y a pas moyen.

Ce que je fais est de transmettre le pointeur UIViewController à l'UIView (ou un héritage approprié). Je suis désolé de ne pas pouvoir vous aider avec l'approche IB du problème car je ne crois pas en IB.

Pour répondre au premier commentateur: parfois, vous avez besoin de savoir qui vous a appelé car cela détermine ce que vous pouvez faire. Par exemple, avec une base de données, vous pouvez avoir un accès en lecture seule ou en lecture / écriture ...


10
Qu'est-ce que cela signifie - "Je ne crois pas en IB"? Je l'ai lancé, il existe certainement.
marcc

5
Vous avez besoin d'une meilleure compréhension du plaisir et de l'abstraction, en particulier en ce qui concerne la langue anglaise. Cela signifie que je n'aime pas ça.
John Smith le

4
Je n'aime pas les avocats. Mais je parie que je peux aider quelqu'un à fabriquer du guacamole. Cela peut être fait dans IB, donc évidemment votre réponse "il n'y a aucun moyen" est incorrecte. Que vous aimiez IB ou non n'a pas d'importance.
Feloneous Cat
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.