Arriver à UIViewController depuis UIView?
y a-t-il une façon intégrée de passer d'un UIView
à son UIViewController
? Je sais que vous pouvez obtenir de UIViewController
à son UIView
via [self view]
mais je me demandais s'il y avait une référence inversée?
24 réponses
comme c'est la réponse acceptée depuis longtemps, je pense que je dois la rectifier avec une meilleure réponse.
Quelques observations sur la nécessité de:
- votre vue ne devrait pas avoir besoin d'accéder directement au contrôleur de vue.
- la vue doit être indépendante du contrôleur de vue et pouvoir fonctionner dans des contextes différents.
- si vous avez besoin d'afficher à l'interface avec le contrôleur de vue, la manière recommandée, et ce que Apple fait à travers Cocoa est d'utiliser le modèle délégué.
un exemple de la façon de le mettre en œuvre est le suivant:
@protocol MyViewDelegate < NSObject >
- (void)viewActionHappened;
@end
@interface MyView : UIView
@property (nonatomic, assign) MyViewDelegate delegate;
@end
@interface MyViewController < MyViewDelegate >
@end
la vue se connecte avec son délégué (comme UITableView
le fait, par exemple) et il ne se soucie pas si elle est implémentée dans le contrôleur de la vue ou dans toute autre classe que vous finissez par utiliser.
ma réponse originale est la suivante: Je ne recommande pas ce, Ni le reste des réponses où l'accès direct au contrôleur de vue est atteint
il n'y a pas de manière intégrée pour le faire. Alors que vous pouvez le contourner en ajoutant un IBOutlet
sur le UIView
et les connecter dans Interface Builder, ce n'est pas recommandé. La vue ne doit pas connaître le contrôleur de vue. Vous devriez plutôt faire comme @Phil M le suggère et créer un protocole à utiliser en tant que délégué.
en utilisant L'exemple posté par Brock, Je l'ai modifié pour qu'il soit une catégorie D'UIView à la place D'UIViewController et l'ai fait récursif pour que n'importe quel subview puisse (avec un peu de chance) trouver le UIViewController parent.
@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 (je l'ai appelé mine" UIKitCategories") et supprimez les données de classe... copiez l'interface @dans l'en-tête, et l'implémentation @dans le .m de fichier. Puis dans votre projet, # importer " UIKitCategories.h" et utilisation dans le code uivi:
// from a UIView subclass... returns nil if UIViewController not available
UIViewController * myController = [self firstAvailableUIViewController];
UIView
est une sous classe de UIResponder
. UIResponder
décrit la méthode -nextResponder
avec une implémentation qui retourne nil
. UIView
remplace cette méthode, comme documenté dans UIResponder
(pour une raison quelconque au lieu de UIView
) comme suit: si la vue a un contrôleur de vue, il est retourné par -nextResponder
. S'il n'y a pas de contrôleur de vue, la méthode retournera la superview.
Ajouter à 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
a une méthode de travail pour retourner le contrôleur de vue.
je suggérerais une approche plus légère pour traverser la chaîne de répondeur complète sans avoir à ajouter une catégorie sur uivi:
@implementation MyUIViewSubclass
- (UIViewController *)viewController {
UIResponder *responder = self;
while (![responder isKindOfClass:[UIViewController class]]) {
responder = [responder nextResponder];
if (nil == responder) {
break;
}
}
return (UIViewController *)responder;
}
@end
combinant plusieurs réponses déjà données, je l'expédie aussi bien 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 j'expédie sur chaque application que je crée. Il a été testé plusieurs fois et je n'ai trouvé aucun problème ou fuite.
P. S.: vous n'avez pas besoin d'utiliser une catégorie comme je l'ai fait si la vue concernée est une de vos sous-classes. Dans ce dernier cas, il suffit de mettre le méthode dans votre sous-classe, et vous êtes bon pour aller.
même si cela peut techniquement être résolu comme pgb recommande, IMHO, il s'agit d'un défaut de conception. Il n'est pas nécessaire que la vue soit au courant du contrôleur.
j'ai modifié de réponse si je peux passer n'importe quel affichage, bouton, label, etc. c'est 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;
}
Edit 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)!
}
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)!
}
}
N'oubliez pas que vous pouvez accéder au contrôleur de vue racine pour la fenêtre dont la vue est un sous-vue. De là, si vous utilisez, par exemple, un contrôleur de vue de navigation et que vous voulez y pousser une nouvelle vue:
[[[[self window] rootViewController] navigationController] pushViewController:newController animated:YES];
vous aurez besoin de configurer la propriété rootViewController de la fenêtre correctement en premier, cependant. Faites ceci 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];
}
alors que ces réponses sont techniquement correctes, y compris Ushox, je pense que la approuvé façon est de mettre en œuvre un nouveau protocole ou de réutiliser un existant. Un protocole isole l'observateur de l'observé, un peu comme mettre une fente pour le courrier entre eux. En effet, C'est ce que Gabriel fait via l'invocation de la méthode pushViewController; la vue "sait" qu'il est correct de demander poliment à votre navigationController de pousser une vue, puisque le viewController conforme au protocole navigationController. Alors que vous pouvez créer votre propre protocole, utilisez simplement L'exemple de Gabriel et réutilisez le protocole UINavigationController.
Je ne pense pas que ce soit une" mauvaise " idée de découvrir qui est le contrôleur de vue pour certains cas. Ce qui pourrait être une mauvaise idée est de sauvegarder la référence de ce controller car il pourrait changer juste comme les superviews changent. Dans mon cas, j'ai un getter qui traverse la chaîne des intervenants.
//.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;
}
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;
}
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
).
alors que cela fonctionne très bien dans l'iPad (le UIPopoverController
se présente, donc n'a pas besoin de référence à un UIViewController
), obtenir le même code à travailler signifie soudainement se référer à votre presentViewController
de votre UIViewController
. Un peu incohérent droit?
comme mentionné précédemment, ce n'est pas la meilleure approche pour avoir la logique dans votre uivi. Mais il a semblé vraiment inutile d'envelopper les quelques lignes de code nécessaires dans un contrôleur séparé.
dans tous les cas, 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
}
}
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 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"
À votre avis d'appel postNotificationName:
- (IBAction) copyString:(id)sender
{
[[NSNotificationCenter defaultCenter] postNotificationName:SLCopyStringNotification object:nil];
}
puis dans votre contrôleur de vue vous ajoutez un observateur. Je le fais ce dans le 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 décrit 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 ça, ça 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 faire remonter l'événement à la UIViewController pour que je puisse faire quelque chose avec les données.
c'est sûrement une mauvaise idée et un mauvais design, mais je suis sûr que nous pouvons tous profiter D'une solution rapide 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. Je stocke Personnellement 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) {}
tous les crédits to @Phil_M
à la réponse de Phil:
dans la ligne: id nextResponder = [self nextResponder];
si self(UIView) n'est pas un sous-View de la vue de ViewController, si vous connaissez la hiérarchie de self(UIView) vous pouvez utiliser aussi: id nextResponder = [[self superview] nextResponder];
...
peut-être que je suis en retard ici. Mais dans cette situation, je n'aime pas la catégorie (pollution). J'aime cette façon:
#define UIViewParentController(__view) ({ \
UIResponder *__responder = __view; \
while ([__responder isKindOfClass:[UIView class]]) \
__responder = [__responder nextResponder]; \
(UIViewController *)__responder; \
})
ma solution serait probablement considérée comme une sorte de faux, mais j'avais une situation similaire à mayoneez (je voulais changer de point de vue en réponse à un geste dans un EAGLView), et j'ai obtenu le contrôleur de point de vue EAGL de cette façon:
EAGLViewController *vc = ((EAGLAppDelegate*)[[UIApplication sharedApplication] delegate]).viewController;
je pense qu'il y a un cas où les besoins observés à informer l'observateur.
je vois un problème similaire lorsque L'UIView dans un Controller UIViewController répond à une situation et qu'il doit d'abord dire à son contrôleur de vue parent de cacher le bouton back et ensuite, une fois terminé, dire au contrôleur de vue parent qu'il doit se détacher de la pile.
j'ai essayé avec des délégués sans succès.
I ne comprends pas pourquoi ce devrait être une mauvaise idée?
une autre façon facile est d'avoir votre propre classe de vue et d'ajouter une propriété du contrôleur de vue dans la classe de vue. Habituellement le controller view crée la vue et c'est là que le controller peut se mettre à la propriété. Au fond, c'est la même chose au lieu de chercher (avec un peu de piratage) le contrôleur, en lui demandant de se positionner sur la vue - c'est simple mais logique car c'est le contrôleur qui "contrôle" la vue.
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)
}
Swift 4 version
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{
}
solution plus rapide
extension UIView {
var parentViewController: UIViewController? {
for responder in sequence(first: self, next: { "151900920".next }) {
if let viewController = responder as? UIViewController {
return viewController
}
}
return nil
}
}
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 Classe de contrôleur de vue requise.
USAGE:
RequiredViewController* rvc = [Utilities getNearestViewController:[RequiredViewController class]];
il n'y a pas moyen.
ce que je fais c'est passer le pointeur UIViewController à L'UIView (ou un héritage approprié). Je suis désolé de ne pas pouvoir aider L'IB à aborder le problème parce que je ne crois pas en L'IB.
pour répondre au premier commentateur: parfois, vous avez besoin de savoir qui vous a appelé parce qu'il détermine ce que vous pouvez faire. Par exemple, avec une base de données, vous pouvez avoir accès en lecture seule ou en lecture/écriture ...