ios oublié - Accéder à UIViewController depuis UIView?




code verrouillage (19)

Existe-t-il un moyen intégré 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 a une référence inverse?


Answers

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

J'ai modifié réponse afin que je puisse passer n'importe quelle vue, bouton, étiquette etc. pour obtenir 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;
}

Modifier la version 3 de Swift

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 rapide

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 une sous-vue. De là, si vous utilisez par exemple un contrôleur de vue de navigation et que vous voulez 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. Pour ce faire, créez d'abord le contrôleur, par exemple dans le délégué de votre 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];
}

À 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 aussi utiliser: id nextResponder = [[self superview] nextResponder]; ...


Cela ne répond pas directement à la question, mais fait plutôt une supposition 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, par exemple le contrôleur de vue, vous pouvez utiliser 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 appel postNotificationName:

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

Ensuite, dans votre contrôleur de vue, vous ajoutez un observateur. Je fais ça 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 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 cela, mais cela semble plus propre que de lancer la première chaîne de réponse. J'ai utilisé ce code pour implémenter un UIMenuController sur un UITableView et transmettre l'événement à UIViewController afin que je puisse faire quelque chose avec les données.


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

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

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 au fur et à mesure que les vues d'ensemble changent. Dans mon cas, j'ai un getter qui traverse la chaîne répondeur.

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

UIView est une sous-classe de UIResponder . UIResponder définit 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 dans 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 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 a une méthode de travail pour retourner le contrôleur de vue.


Il n'y a pas moyen.

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

Pour répondre au premier intervenant: il faut parfois savoir qui vous a appelé parce que 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 ...


Même si cela peut techniquement être résolu comme le recommande pgb , à mon humble avis , c'est un défaut de conception. La vue ne devrait pas avoir besoin d'être au courant du contrôleur.


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 (ce n'est vraiment pas beaucoup plus qu'un bouton qui ouvre un PopoverController ).

Bien que cela fonctionne bien dans l'iPad (le UIPopoverController présente donc sans référence à un UIViewController ), faire fonctionner le même code signifie faire référence soudainement à votre UIViewController depuis votre UIViewController . Kinda incompatible droit?

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

De toute façon, 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
    }
}

Bien que ces réponses soient techniquement correctes, y compris Ushox, je pense que la façon 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 bon protocole pour demander poliment à votre navigationController de pousser une vue, puisque viewController est conforme au protocole navigationController. Alors que vous pouvez créer votre propre protocole, il suffit d'utiliser l'exemple de Gabriel et de réutiliser le protocole UINavigationController.


Un autre moyen 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. Généralement, le contrôleur de vue crée la vue et c'est là que le contrôleur peut se positionner sur la propriété. Fondamentalement, c'est au lieu de chercher (avec un peu de piratage) pour le contrôleur, que le contrôleur se positionne sur la vue - c'est simple mais logique car c'est le contrôleur qui "contrôle" la vue.


En combinant plusieurs réponses déjà données, je m'en charge é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 ARC-enabled que je livre sur chaque application que je crée. Il a été testé plusieurs fois et je n'ai trouvé aucun problème ou 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 le dernier cas, il suffit de mettre la méthode dans votre sous-classe et vous êtes prêt à partir.


En utilisant l'exemple publié par Brock, je l'ai modifié pour qu'il soit une catégorie de UIView à la place de UIViewController et qu'il soit récursif pour que n'importe quelle 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 l'@interface dans l'en-tête et la @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];

Puisque cela a été la réponse acceptée depuis longtemps, je pense que j'ai besoin de 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 être capable de travailler dans différents contextes.
  • Si vous avez besoin de la vue pour interfacer avec le contrôleur de vue, la méthode recommandée, et ce que Apple fait à travers Cocoa est d'utiliser le modèle de délégué.

Un exemple de la façon de l'implémenter est le suivant:

@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 le fait UITableView , par exemple) et il ne se soucie pas 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ù l'accès direct au contrôleur de vue est atteint

Il n'y a pas de façon intégrée de le faire. Bien que vous puissiez le contourner en ajoutant un IBOutlet sur l' UIView et en le connectant dans Interface Builder, ceci n'est pas recommandé. La vue ne doit pas connaître le contrôleur de vue. Au lieu de cela, vous devriez faire ce que @Phil M suggère et créer un protocole à utiliser en tant que délégué.


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


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

Je vois un problème similaire où UIView dans un UIViewController répond à une situation et il doit d'abord dire à son contrôleur de vue parent pour masquer le bouton de retour, puis à la fin dire au contrôleur de vue parent qu'il doit se décoller de la pile.

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

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


J'ai mis en place une sous-classe UIScrollView , UITableView et même UICollectionView universelle, qui permet de déplacer tous les champs de texte à l'intérieur du clavier.

Lorsque le clavier est sur le point d'apparaître, la sous-classe trouvera la sous-vue sur le point d'être modifiée et ajustera son image et son contenu pour s'assurer que la vue est visible, avec une animation correspondant à la fenêtre contextuelle du clavier. Lorsque le clavier disparaît, il restaure sa taille antérieure.

Il devrait fonctionner avec n'importe quelle configuration, soit une interface UITableView , soit une vue manuelle.

Voici une solution pour déplacer les champs de texte hors du clavier





ios objective-c cocoa-touch uiview uiviewcontroller