Étant donné une vue, comment obtenir son viewController?

141

J'ai un pointeur vers un fichier UIView. Comment y accéder UIViewController? [self superview]est un autre UIView, mais pas le UIViewController, non?

Mahboudz
la source
Je pense que ce fil a les réponses: Accéder à UIViewController depuis UIView sur iPhone?
Ushox
Fondamentalement, j'essaie d'appeler la vue de mon contrôleur viewWillAppear lorsque ma vue est rejetée. La vue est rejetée par la vue elle-même qui détecte un tap et appelle [self removeFromSuperview]; ViewController n'appelle pas viewWillAppear / WillDisappear / DidAppear / DidDisappear lui-même.
mahboudz
Je voulais dire que j'essaye d'appeler viewWillDisappear car ma vue est rejetée.
mahboudz le
1
Possible duplication de Get to UIViewController depuis UIView?
Efren

Réponses:

42

Oui, superviewc'est la vue qui contient votre vue. Votre vue ne doit pas savoir quel est exactement son contrôleur de vue, car cela enfreindrait les principes MVC.

Le contrôleur, quant à lui, sait de quelle vue il est responsable ( self.view = myView), et généralement, cette vue délègue des méthodes / événements à gérer au contrôleur.

En règle générale, au lieu d'un pointeur vers votre vue, vous devriez avoir un pointeur vers votre contrôleur, qui à son tour peut soit exécuter une logique de contrôle, soit transmettre quelque chose à sa vue.

Dimitar Dimitrov
la source
24
Je ne sais pas si cela enfreindrait les principes MVC. À tout moment, une vue n'a qu'un seul contrôleur de vue. Pouvoir y accéder pour lui renvoyer un message devrait être une fonctionnalité automatique, et non une fonctionnalité pour laquelle vous devez travailler (en ajoutant une propriété pour garder une trace). On pourrait dire la même chose des vues: pourquoi avez-vous besoin de savoir qui vous êtes l'enfant? Ou s'il existe d'autres points de vue frères et sœurs. Pourtant, il existe des moyens d'obtenir ces objets.
mahboudz le
Vous avez un peu raison sur la vue, connaissant son parent, ce n'est pas une décision de conception très claire, mais il est déjà établi pour effectuer certaines actions, en utilisant directement une variable de membre superview (vérifier le type de parent, supprimer du parent, etc.). Ayant récemment travaillé avec PureMVC, je suis devenu un peu plus pointilleux sur l'abstraction du design :) Je ferais un parallèle entre les classes UIView et UIViewController de l'iPhone et les classes View et Mediator de PureMVC - la plupart du temps, la classe View n'a pas besoin de connaître son gestionnaire / interface MVC (UIViewController / Mediator).
Dimitar Dimitrov le
9
Mot clé: "le plus".
Glenn Maynard
278

De la UIResponderdocumentation pour nextResponder:

La classe UIResponder ne stocke pas ou ne définit pas automatiquement le répondeur suivant, mais renvoie nil par défaut. Les sous-classes doivent remplacer cette méthode pour définir le prochain répondeur.UIView implémente cette méthode en retournant l'objet UIViewController qui le gère (s'il en a un) ou son superview (si ce n'est pas le cas) ; UIViewController implémente la méthode en renvoyant la supervision de sa vue; UIWindow renvoie l'objet application et UIApplication renvoie nil.

Ainsi, si vous recurse une vue nextResponderjusqu'à ce qu'elle soit de type UIViewController, alors vous avez le viewController parent de n'importe quelle vue.

Notez qu'il peut toujours pas avoir de contrôleur de vue parent. Mais seulement si la vue ne fait pas partie de la hiérarchie de vues d'un viewController.

Swift 3 et Extension Swift 4.1 :

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

Extension Swift 2:

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

Catégorie Objectif-C:

@interface UIView (mxcl)
- (UIViewController *)parentViewController;
@end

@implementation UIView (mxcl)
- (UIViewController *)parentViewController {
    UIResponder *responder = self;
    while ([responder isKindOfClass:[UIView class]])
        responder = [responder nextResponder];
    return (UIViewController *)responder;
}
@end

Cette macro évite la pollution de catégorie:

#define UIViewParentController(__view) ({ \
    UIResponder *__responder = __view; \
    while ([__responder isKindOfClass:[UIView class]]) \
        __responder = [__responder nextResponder]; \
    (UIViewController *)__responder; \
})
mxcl
la source
Juste une chose: si vous vous inquiétez de la pollution de catégorie, définissez-la simplement comme une fonction statique plutôt que comme une macro. De plus, le typage brutal est dangereux, et en plus de cela, la macro peut ne pas être correcte mais je ne suis pas sûr.
mojuba
La macro fonctionne, je n'utilise que la version macro personnellement. Je démarre beaucoup de projets et j'ai un en-tête que je viens de déposer partout avec un tas de ces fonctions utilitaires macro. Cela me fait gagner du temps. Si vous n'aimez pas les macros, vous pouvez les adapter en fonction, mais les fonctions statiques semblent fastidieuses, car vous devez ensuite en mettre une dans chaque fichier que vous souhaitez utiliser. On dirait que vous voudriez plutôt une fonction non statique déclarée dans un en-tête et définie dans un .m quelque part?
mxcl
Bien pour éviter certains délégués ou notifications. Merci!
Ferran Maylinch
Vous devriez en faire une extension de UIResponder;). Article très édifiant.
ScottyBlades
32

@andrey répond en une ligne (testé dans Swift 4.1 ):

extension UIResponder {
    public var parentViewController: UIViewController? {
        return next as? UIViewController ?? next?.parentViewController
    }
}

usage:

 let vc: UIViewController = view.parentViewController
Federico Zanetello
la source
parentViewControllerne peut pas être défini publicsi l'extension est dans le même fichier que UIViewvous pouvez la définir fileprivate, elle compile mais cela ne fonctionne pas! 😐
Cela fonctionne bien. Je l'ai utilisé pour l'action du bouton retour dans le fichier .xib d'en-tête commun.
McDonal_11
23

À des fins de débogage uniquement, vous pouvez appeler _viewDelegate vues pour obtenir leurs contrôleurs de vue. C'est une API privée, donc pas sûre pour l'App Store, mais pour le débogage, c'est utile.

Autres méthodes utiles:

  • _viewControllerForAncestor- obtenir le premier contrôleur qui gère une vue dans la chaîne superview. (merci n00neimp0rtant)
  • _rootAncestorViewController - récupère le contrôleur ancêtre dont la hiérarchie de vue est actuellement définie dans la fenêtre.
Leo Natan
la source
C'est exactement pourquoi je suis venu à cette question. Apparemment, «nextResponder» fait la même chose, mais j'apprécie la perspicacité que cette réponse fournit. Je comprends et j'aime MVC, mais le débogage est un autre animal!
mbm29414
4
Il semble que cela ne fonctionne que sur la vue principale du contrôleur de vue, pas sur les sous-vues de celui-ci. _viewControllerForAncestorparcourra les super-vues jusqu'à ce qu'il trouve la première qui appartient à un contrôleur de vue.
n00neimp0rtant
Merci @ n00neimp0rtant! Je vote pour cette réponse pour que les gens voient votre commentaire.
eyuelt
Mettez à jour la réponse avec plus de méthodes.
Leo Natan
1
Ceci est utile lors du débogage.
evanchin le
10

Pour obtenir une référence à UIViewController ayant UIView, vous pouvez faire une extension de UIResponder (qui est une super classe pour UIView et UIViewController), ce qui permet de remonter dans la chaîne de répondeurs et d'atteindre ainsi UIViewController (sinon en retournant nil).

extension UIResponder {
    func getParentViewController() -> UIViewController? {
        if self.nextResponder() is UIViewController {
            return self.nextResponder() as? UIViewController
        } else {
            if self.nextResponder() != nil {
                return (self.nextResponder()!).getParentViewController()
            }
            else {return nil}
        }
    }
}

//Swift 3
extension UIResponder {
    func getParentViewController() -> UIViewController? {
        if self.next is UIViewController {
            return self.next as? UIViewController
        } else {
            if self.next != nil {
                return (self.next!).getParentViewController()
            }
            else {return nil}
        }
    }
}

let vc = UIViewController()
let view = UIView()
vc.view.addSubview(view)
view.getParentViewController() //provide reference to vc
Andreï
la source
4

La manière rapide et générique dans Swift 3:

extension UIResponder {
    func parentController<T: UIViewController>(of type: T.Type) -> T? {
        guard let next = self.next else {
            return nil
        }
        return (next as? T) ?? next.parentController(of: T.self)
    }
}

//Use:
class MyView: UIView {
    ...
    let parentController = self.parentController(of: MyViewController.self)
}
iTSangar
la source
2

Si vous n'êtes pas familier avec le code et que vous souhaitez trouver ViewController correspondant à une vue donnée, vous pouvez essayer:

  1. Exécuter l'application dans le débogage
  2. Accédez à l'écran
  3. Commencer l'inspecteur de vue
  4. Saisissez la vue que vous souhaitez rechercher (ou une vue enfant encore mieux)
  5. Dans le volet de droite, obtenez l'adresse (par exemple 0x7fe523bd3000)
  6. Dans la console de débogage, commencez à écrire des commandes:
    po (UIView *) 0x7fe523bd3000
    po [(UIView *) 0x7fe523bd3000 nextResponder]
    po [[(UIView *) 0x7fe523bd3000 nextResponder] nextResponder]
    po [[[(UIView *) 0x7fe523bd3000 nextResponder] nextResponder] nextResponder]
    ...

Dans la plupart des cas, vous obtiendrez UIView, mais de temps en temps, il y aura une classe basée sur UIViewController.

m4js7er
la source
1

Je pense que vous pouvez propager le robinet au contrôleur de vue et le laisser le gérer. C'est une approche plus acceptable. En ce qui concerne l'accès à un contrôleur de vue depuis sa vue, vous devez conserver une référence à un contrôleur de vue, car il n'y a pas d'autre moyen. Voir ce fil, cela peut aider: Accéder au contrôleur de vue à partir d'une vue

Nava Carmon
la source
Si vous avez une poignée de vues et que l'on ferme toutes les vues et que vous devez appeler viewWillDisappear, ne serait-il pas plus facile pour cette vue de détecter le robinet que de remettre le robinet au contrôleur de vue et de faire vérifier le contrôleur de vue avec toutes les vues pour voir laquelle a été tapée?
mahboudz le
0

Plus de code de sécurité de type pour Swift 3.0

extension UIResponder {
    func owningViewController() -> UIViewController? {
        var nextResponser = self
        while let next = nextResponser.next {
            nextResponser = next
            if let vc = nextResponser as? UIViewController {
                return vc
            }
        }
        return nil
    }
}
Denis Krivitski
la source
0

Hélas, cela est impossible à moins que vous ne sous-classiez la vue et ne lui fournissiez une propriété d'instance ou similaire qui stocke la référence du contrôleur de vue à l'intérieur une fois que la vue est ajoutée à la scène ...

Dans la plupart des cas, il est très facile de contourner le problème d'origine de ce message car la plupart des contrôleurs de vue sont des entités bien connues du programmeur qui était responsable de l'ajout de toutes les sous-vues à la vue de ViewController ;-) C'est pourquoi je suppose qu'Apple jamais pris la peine d'ajouter cette propriété.

Morten Carlsen
la source
0

Un peu tard, mais voici une extension qui vous permet de trouver un répondeur de tout type, y compris ViewController.

extension NSObject{
func findNext(type: AnyClass) -> Any{
    var resp = self as! UIResponder

    while !resp.isKind(of: type.self) && resp.next != nil
    {
        resp = resp.next!
    }

    return resp
  }                       
}
Hackbarth
la source
-1

Si vous définissez un point d'arrêt, vous pouvez le coller dans le débogueur pour imprimer la hiérarchie des vues:

po [[UIWindow keyWindow] recursiveDescription]

Vous devriez pouvoir trouver le parent de votre vue quelque part dans ce désordre :)

Scott McCoy
la source
recursiveDescriptionimprime uniquement la hiérarchie des vues , pas les contrôleurs de vue.
Alan Zeino
1
à partir de là, vous pouvez identifier le viewcontroller @AlanZeino
Akshay