web-dev-qa-db-fra.com

Obtenir UIViewController d'UIView?

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

179
bertrandom

Cette réponse étant acceptée depuis longtemps, j’ai le sentiment que je devrais y remédier 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 plutôt être indépendante du contrôleur de vue et pouvoir fonctionner dans différents contextes.
  • Si vous avez besoin que la vue s'interface avec le contrôleur de vue, la méthode recommandée et ce que Apple fait dans Cocoa consiste à 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, par exemple) et ne se soucie pas de savoir si sa mise en œuvre est implémentée dans le contrôleur de vue ou dans toute autre classe que vous utilisez.

Ma réponse initiale est la suivante: Je ne le recommande pas, pas plus que le reste des réponses lorsque l'accès direct au contrôleur de vue est obten

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

45
pgb

En utilisant l'exemple posté par Brock, je l'ai modifié pour qu'il s'agisse d'une catégorie d'UIView au lieu de UIViewController et l'a 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 la classe ... copiez le @interface dans l'en-tête et le @implementation dans le fichier .m. Puis dans votre projet, importez #import "UIKitCategories.h" et utilisez-le dans le code UIView:

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

UIView est une sous-classe de UIResponder. UIResponder présente la méthode -nextResponder avec une implémentation qui renvoie nil. UIView remplace cette méthode, comme indiqué dans UIResponder (pour une raison quelconque au lieu de UIView) comme suit: si la vue possède un contrôleur de vue, il est renvoyé par -nextResponder . S'il n'y a pas de contrôleur de vue, la méthode renverra la vue supérieure.

Ajoutez ceci à votre projet et vous êtes prêt à rouler.

@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

Maintenant, UIView dispose d'une méthode de travail pour renvoyer le contrôleur de vue.

112
Brock

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

En combinant plusieurs réponses déjà données, j'expédie également 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 bibliothèque statique activée par ARC que je fournis avec chaque application que je crée. Il a été testé à plusieurs reprises et je n'ai trouvé aucun problème ni aucune fuite.

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

22

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

12
Ushox

J'ai modifié la réponse de pour pouvoir passer n'importe quelle vue, bouton, étiquette, etc., afin d'obtenir le nom de son parent 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;
}

Éditer Swift 3 Version

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

Éditer 2: - Swift Extension

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)!
    }
}
11
Varun Naharia

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 êtes par exemple en utilisant 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 cela lorsque vous créez le contrôleur pour la première fois, par exemple. dans votre délégué app:

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

Je suis tombé par hasard sur une situation dans laquelle je souhaite réutiliser un petit composant et ai ajouté du code dans une vue réutilisable (ce n'est en réalité rien de plus qu'un bouton qui ouvre une PopoverController).

Bien que cela fonctionne bien sur l'iPad (la UIPopoverController se présente, il n'est donc pas nécessaire de faire référence à une UIViewController), mais le même code signifie que vous devez brusquement référencer votre presentViewController à partir de votre UIViewController. Un peu incohérent non?

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

Quoi qu'il en soit, voici une solution Swift, qui ajoute une nouvelle propriété à tout 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
    }
}
6
Kevin R

Bien que ces réponses soient techniquement correctes, y compris Ushox, je pense que la méthode approuvée consiste à mettre en œuvre un nouveau protocole ou à réutiliser un protocole existant. Un protocole isole l'observateur de l'observé, un peu comme si on mettait un emplacement de courrier entre eux. En fait, c'est ce que fait Gabriel via l'invocation de la méthode pushViewController; la vue "sait" qu'il est du protocole approprié de demander poliment à votre navigationController d'afficher une vue, car 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
PapaSmurf

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 d’enregistrer 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;
}
5
Rivera

La plus simple boucle while 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

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

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

C’est sûrement une mauvaise idée et un mauvais design, mais je suis sûr que nous pourrons 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 un dialogue modal ou des données de suivi, 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 en tant que:

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

Tout crédit à @Phil_M

3
Paul Slm

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 entoure AVPlayer/AVPlayerViewController et je souhaite fournir une méthode simple show(in view: UIView) incorporera AVPlayerViewController dans view. Pour cela, je dois accéder à view de UIViewController.

3
frouo

Cela ne répond pas directement à la question, mais suppose plutôt l’intention de l’intention de la question.

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

Commencez par créer 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 le fais 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 procéder, cela semble simplement plus sain que de remonter jusqu'à la chaîne des premiers intervenants. J'ai utilisé ce code pour implémenter un UIMenuController sur un UITableView et transmettre l'événement au UIViewController afin que je puisse faire quelque chose avec les données.

3
Shaolo

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
    }
}
3
Igor Palaguta

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
levin varghese

A la réponse de Phil:

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

1
sag

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)
}
1
Dicle

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

Je vois un problème similaire dans lequel UIView dans un UIViewController répond à une situation et doit d'abord indiquer à son contrôleur de vue parent de masquer le bouton Précédent puis, une fois l'opération terminée, indiquer au contrôleur de vue parent qu'il doit se retirer 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
user7865437

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. Généralement, le contrôleur de vue crée la vue et c’est là que le contrôleur peut s’appliquer lui-même à la propriété. Au lieu de cela, il est au lieu de chercher (avec un peu de piratage) le contrôleur, qui doit se régler lui-même sur la vue - ceci est simple mais logique car c'est le contrôleur qui "contrôle" la vue.

0
jack

Si vous n'allez pas télécharger ceci dans 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
pixelomer

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

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