Arrivare a UIViewController da UIView?


Risposte:


46

Poiché questa è stata la risposta accettata da molto tempo, sento di doverlo rettificare con una risposta migliore.

Alcuni commenti sulla necessità:

  • La tua vista non dovrebbe avere bisogno di accedere direttamente al controller di vista.
  • La vista dovrebbe invece essere indipendente dal controller di vista ed essere in grado di funzionare in contesti diversi.
  • Se hai bisogno che la vista si interfaccia in un modo con il controller di vista, il modo consigliato e ciò che Apple fa su Cocoa è usare il modello delegato.

Di seguito è riportato un esempio di come implementarlo:

@protocol MyViewDelegate < NSObject >

- (void)viewActionHappened;

@end

@interface MyView : UIView

@property (nonatomic, assign) MyViewDelegate delegate;

@end

@interface MyViewController < MyViewDelegate >

@end

La vista si interfaccia con il suo delegato (come UITableViewfa per esempio) e non gli importa se è implementata nel controller di vista o in qualsiasi altra classe che si finisce per usare.

La mia risposta originale segue: Non lo consiglio, né il resto delle risposte in cui si ottiene l'accesso diretto al controller di visualizzazione

Non esiste un modo integrato per farlo. Mentre è possibile aggirare l'ostacolo con l'aggiunta di un IBOutletsulla UIViewe collegando questi in Interface Builder, questo non è raccomandato. La vista non dovrebbe conoscere il controller della vista. Invece, dovresti fare come suggerisce @Phil M e creare un protocollo da utilizzare come delegato.


26
È un pessimo consiglio. Non devi fare riferimento a un controller di visualizzazione da una vista
Philippe Leybaert,

6
@MattDiPasquale: sì, è un cattivo design.
Philippe Leybaert,

23
@Phillipe Leybaert Sono curioso di conoscere i tuoi pensieri sul miglior design per un evento di clic sui pulsanti che dovrebbe invocare alcune azioni sul controller, senza che la vista contenga un riferimento al controller. Grazie
Jonathon Horsman il

3
@PhilippeLeybaert I progetti di esempio di Apple tendono a dimostrare l'utilizzo di particolari funzionalità API. Molti mi sono riferito a sacrificare un design buono o scalabile per fornire una concisa dimostrazione dell'argomento. Mi ci è voluto molto tempo per rendermene conto e, sebbene abbia senso, lo trovo sfortunato. Penso che molti sviluppatori considerino questi progetti pragmatici come la guida di Apple alle migliori pratiche di progettazione, che sono certo che non lo sono.
Benjohn,

11
Tutto questo bs su "non dovresti farlo" è proprio questo. Se una vista vuole sapere del suo controller di vista, spetta al programmatore decidere. periodo.
Daniel Kanaan,

203

Utilizzando l'esempio pubblicato da Brock, l'ho modificato in modo che sia una categoria di UIView anziché UIViewController e l'ho reso ricorsivo in modo che qualsiasi sottoview possa (si spera) trovare il UIViewController principale.

@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

Per usare questo codice, aggiungilo in un nuovo file di classe (ho chiamato il mio "UIKitCategories") e rimuovi i dati della classe ... copia @interface nell'intestazione e @implementation nel file .m. Quindi, nel tuo progetto, #importa "UIKitCategories.h" e utilizza nel codice UIView:

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

47
E uno dei motivi per cui devi consentire a UIView di essere a conoscenza del suo UIViewController è quando hai sottoclassi UIView personalizzate che devono inviare una vista / finestra di dialogo modale.
Phil M,

Fantastico, ho dovuto accedere al mio ViewController per visualizzare un popup personalizzato creato da una sottoview
aryaxt

2
Non è questa cattiva pratica per un UIView di spingere una vista modale? Lo sto facendo in questo momento ma sento che non è la cosa giusta da fare ..
Van Du Tran,

6
Phil, la tua vista personalizzata dovrebbe chiamare un metodo delegato che il controller di visualizzazione ascolta e quindi spinge da lì.
Malhal,

9
adoro quante domande SO ha ottenuto un "saggio", una risposta accademica e accettata con pochi punti e una seconda risposta che è pratica, sporca e contraria alle regole, con dieci volte i punti :-)
hariseldon78

114

UIViewè una sottoclasse di UIResponder. UIResponderespone il metodo -nextRespondercon un'implementazione che ritorna nil. UIViewsovrascrive questo metodo, come documentato in UIResponder(per qualche motivo anziché in UIView) come segue: se la vista ha un controller di vista, viene restituita da -nextResponder. Se non è presente alcun controller di visualizzazione, il metodo restituirà la superview.

Aggiungi questo al tuo progetto e sei pronto per iniziare.

@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

Ora UIViewha un metodo di lavoro per restituire il controller di visualizzazione.


5
Funzionerà solo se non c'è nulla nella catena di responder tra la ricezione UIViewe la UIViewController. La risposta di Phil M con la ricorsione è la strada da percorrere.
Olivier,

33

Suggerirei un approccio più leggero per attraversare l'intera catena di responder senza dover aggiungere una categoria su 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

Combinando diverse risposte già fornite, ci sto spedendo anche con la mia implementazione:

@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 categoria fa parte della mia libreria statica abilitata per ARC che invio su ogni applicazione che creo. È stato testato più volte e non ho riscontrato problemi o perdite.

PS: Non è necessario utilizzare una categoria come ho fatto se la vista interessata è una sottoclasse della tua. In quest'ultimo caso, inserisci il metodo nella sottoclasse e sei a posto.


1
Questa è la risposta migliore Non c'è bisogno di ricorsione, questa versione è ben ottimizzata
zeroimpl

12

Anche se questo può tecnicamente essere risolto come consiglia pgb , IMHO, questo è un difetto di progettazione. Non è necessario che la vista sia a conoscenza del controller.


Non sono sicuro di come si possa dire a viewController che una delle sue viste sta scomparendo e che deve chiamare uno dei suoi metodi viewXXXAppear / viewXXXDisappear.
Mahboudz,

2
Questa è l'idea alla base del modello Observer. L'Osservato (la Vista in questo caso) non dovrebbe essere direttamente a conoscenza dei suoi Osservatori. L'Osservatore dovrebbe ricevere solo i callback a cui sono interessati.
Ushox,

12

Ho modificato de risposta così posso passare qualsiasi, pulsante di vista, etichettare ecc per farlo con i genitori UIViewController. Ecco il mio codice

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

Modifica la versione di 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)!
    }

Modifica 2: - Estensione rapida

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

Non dimenticare che puoi accedere al controller della vista principale per la finestra di cui la vista è una sottoview. Da lì, se ad esempio stai usando un controller di visualizzazione della navigazione e vuoi spingere una nuova vista su di esso:

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

Tuttavia, dovrai prima impostare correttamente la proprietà rootViewController della finestra. Fallo quando crei per la prima volta il controller, ad esempio nel delegato dell'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];
}

Mi sembra che, in conformità con i documenti Apple, dato che [[self navigationController] view]è la vista (principale) "principale" della finestra, la rootViewControllerproprietà della finestra deve essere impostata su navigationControllerquale controlla immediatamente la vista "principale".
adubr,

6

Sebbene queste risposte siano tecnicamente corrette, incluso Ushox, penso che il modo approvato sia quello di implementare un nuovo protocollo o di riutilizzarne uno esistente. Un protocollo isola l'osservatore dall'osservato, un po 'come mettere uno slot di posta tra di loro. In effetti, questo è ciò che Gabriel fa tramite l'invocazione del metodo pushViewController; la vista "sa" che è un protocollo appropriato chiedere educatamente al tuo NavigationController di inviare una vista, poiché viewController è conforme al protocollo navigationController. Mentre puoi creare il tuo protocollo, basta usare l'esempio di Gabriel e riutilizzare il protocollo UINavigationController va bene.


6

Mi sono imbattuto in una situazione in cui ho un piccolo componente che voglio riutilizzare e ho aggiunto un po 'di codice in una vista riutilizzabile stessa (in realtà non è molto più di un pulsante che apre a PopoverController).

Mentre questo funziona bene sull'iPad (lo UIPopoverControllerstesso presenta, quindi non ha bisogno di alcun riferimento a UIViewController), ottenere lo stesso codice per funzionare significa improvvisamente fare riferimento al tuo presentViewControllerdal tuo UIViewController. Incoerente vero?

Come accennato in precedenza, non è l'approccio migliore per avere la logica in UIView. Ma è stato davvero inutile racchiudere le poche righe di codice necessarie in un controller separato.

Ad ogni modo, ecco una soluzione rapida, che aggiunge una nuova proprietà a qualsiasi 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

Non penso che sia "cattiva" idea scoprire chi è il controller di visualizzazione per alcuni casi. Quale potrebbe essere una cattiva idea è quella di salvare il riferimento a questo controller in quanto potrebbe cambiare proprio come cambiano le panoramiche. Nel mio caso ho un getter che attraversa la catena dei soccorritori.

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

Il ciclo do do più semplice per trovare 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

(più conciso delle altre risposte)

fileprivate extension UIView {

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

}

Il mio caso uso per cui ho bisogno di accedere alla vista prima UIViewController: ho un oggetto che avvolge AVPlayer/ AVPlayerViewControllere voglio fornire un semplice show(in view: UIView)metodo che incorporare AVPlayerViewControllerin view. Per questo, ho bisogno di accesso view's UIViewController.


3

Questo non risponde direttamente alla domanda, ma piuttosto fa un'ipotesi sull'intenzione della domanda.

Se si dispone di una vista e in quella vista è necessario chiamare un metodo su un altro oggetto, ad esempio il controller di vista, è possibile utilizzare invece NSNotificationCenter.

Innanzitutto crea la stringa di notifica in un file di intestazione

#define SLCopyStringNotification @"ShaoloCopyStringNotification"

A tuo avviso chiama postNotificationName:

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

Quindi nel tuo controller di visualizzazione aggiungi un osservatore. Faccio questo in viewDidLoad

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

Ora (anche nello stesso controller di visualizzazione) implementa il tuo metodo copyString: come illustrato nel @selector sopra.

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

Non sto dicendo che questo è il modo giusto per farlo, sembra solo più pulito rispetto alla prima catena di responder. Ho usato questo codice per implementare un UIMenuController su un UITableView e passare l'evento di nuovo su UIViewController in modo da poter fare qualcosa con i dati.


3

È sicuramente una cattiva idea e un design sbagliato, ma sono sicuro che tutti potremo godere di una soluzione Swift della migliore risposta proposta da @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)
}

Se la tua intenzione è fare cose semplici, come mostrare una finestra di dialogo modale o tenere traccia dei dati, ciò non giustifica l'uso di un protocollo. Memorizzo personalmente questa funzione in un oggetto di utilità, è possibile utilizzarla da qualsiasi cosa che implementa il protocollo UIResponder come:

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

Tutto il merito a @Phil_M


3

Forse sono in ritardo qui. Ma in questa situazione non mi piace la categoria (inquinamento). Adoro così:

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

3

Soluzione più veloce

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

Mi piace questa risposta. Non sono sicuro che sia più swifty però. Swift è ancora-un altro-non-non-decidere-quale-di-molti-paradigmi-vuole-essere-sposato-con le lingue. In questo caso, hai una bella dimostrazione di un approccio più funzionale (il sequencebit). Quindi se "swifty" significa "più funzionale", allora suppongo che sia più veloce.
Travis Griggs,

2

Versione aggiornata per swift 4: grazie per @Phil_M e @ 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

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

Esempio di utilizzo

 if let parent = self.view.parentViewController{

 }

2

Due soluzioni a partire da Swift 5.2 :

  • Più dal punto di vista funzionale
  • Non è necessario per la returnparola chiave ora 🤓

Soluzione 1:

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

Soluzione 2:

extension UIView {
    var parentViewController: UIViewController? {
        sequence(first: self) { $0.next }
            .compactMap{ $0 as? UIViewController }
            .first
    }
}
  • Questa soluzione richiede prima di tutto l'iterazione attraverso ciascun risponditore, quindi potrebbe non essere il più performante.

1

Alla risposta di Phil:

In linea: id nextResponder = [self nextResponder]; se self (UIView) non è una sottoview della vista di ViewController, se conosci la gerarchia di self (UIView) puoi usare anche: id nextResponder = [[self superview] nextResponder];...


0

La mia soluzione sarebbe probabilmente considerata una specie di falso ma ho avuto una situazione simile a quella di mayoneez (volevo cambiare visuale in risposta a un gesto in un EAGLView), e ho ottenuto il controller di vista dell'EAGL in questo modo:

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

2
Riscrivere il codice come segue: EAGLViewController *vc = [(EAGLAppDelegate *)[UIApplication sharedApplication].delegate viewController];.
Jonathan Sterling,

1
Il problema non è con la sintassi del punto, ma con i tipi. Nell'obiettivo C per dichiarare un oggetto che scrivi ClassName *object, con un asterisco.
adubr,

Oops ... in realtà è quello che avevo, ma il widget HTML StackOverflow sembra che pensasse che l'asterisco significasse corsivo ... L'ho cambiato in un blocco di codice, ora viene visualizzato correttamente. Grazie!
gulchrider,

0

Penso che ci sia un caso in cui l'osservato deve informare l'osservatore.

Vedo un problema simile in cui l'UIView in UIViewController sta rispondendo a una situazione e deve prima dire al controller della vista padre di nascondere il pulsante Indietro e poi, al termine, dire al controllore della vista padre che deve uscire dallo stack.

Ho provato questo con i delegati senza successo.

Non capisco perché questa dovrebbe essere una cattiva idea?


0

Un altro modo semplice è avere la propria classe di vista e aggiungere una proprietà del controller di vista nella classe di vista. Di solito il controller della vista crea la vista ed è qui che il controller può impostarsi sulla proprietà. Fondamentalmente è invece di cercare (con un po 'di hacking) il controller, avendo il controller per impostarsi sulla vista - questo è semplice ma ha senso perché è il controller che "controlla" la vista.


0

Se non hai intenzione di caricarlo su App Store, puoi anche utilizzare un metodo privato di 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
}

Sebbene questo codice possa rispondere alla domanda, una buona risposta dovrebbe anche spiegare cosa fa il codice e come risolve il problema.
BDL il

0

Per ottenere il controller di una determinata vista, è possibile utilizzare la catena UIFirstResponder.

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

-1

Se rootViewController è UINavigationViewController, impostato nella classe AppDelegate, quindi

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

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

return nil;}

Dove c richiesto, visualizzare la classe dei controller.

USO:

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

-5

Non c'è alcun modo.

Quello che faccio è passare il puntatore UIViewController a UIView (o un'eredità appropriata). Mi dispiace non poterti aiutare con l'approccio IB al problema perché non credo nell'IB.

Per rispondere al primo commentatore: a volte devi sapere chi ti ha chiamato perché determina cosa puoi fare. Ad esempio con un database potresti avere solo accesso in lettura o lettura / scrittura ...


10
Cosa significa "Non credo in IB"? L'ho lanciato, esiste sicuramente.
marzo

5
Hai bisogno di una migliore comprensione del divertimento e dell'astrazione, soprattutto per quanto riguarda la lingua inglese. Significa che non mi piace.
John Smith,

4
Non mi piacciono gli avocado. Ma scommetto che posso aiutare qualcuno a preparare il guacamole. Può essere fatto in IB, quindi ovviamente la tua risposta di "non c'è modo" non è corretta. Se ti piace IB o no è irrilevante.
Feloneous Cat,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.