C'è un modo integrato per andare da a UIView
a suo UIViewController
? So che puoi arrivare dalla UIViewController
sua UIView
via [self view]
ma mi chiedevo se ci fosse un riferimento inverso?
C'è un modo integrato per andare da a UIView
a suo UIViewController
? So che puoi arrivare dalla UIViewController
sua UIView
via [self view]
ma mi chiedevo se ci fosse un riferimento inverso?
Risposte:
Poiché questa è stata la risposta accettata da molto tempo, sento di doverlo rettificare con una risposta migliore.
Alcuni commenti sulla necessità:
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 UITableView
fa 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 IBOutlet
sulla UIView
e 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.
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];
UIView
è una sottoclasse di UIResponder
. UIResponder
espone il metodo -nextResponder
con un'implementazione che ritorna nil
. UIView
sovrascrive 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 UIView
ha un metodo di lavoro per restituire il controller di visualizzazione.
UIView
e la UIViewController
. La risposta di Phil M con la ricorsione è la strada da percorrere.
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
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.
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.
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)!
}
}
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];
}
[[self navigationController] view]
è la vista (principale) "principale" della finestra, la rootViewController
proprietà della finestra deve essere impostata su navigationController
quale controlla immediatamente la vista "principale".
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.
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 UIPopoverController
stesso presenta, quindi non ha bisogno di alcun riferimento a UIViewController
), ottenere lo stesso codice per funzionare significa improvvisamente fare riferimento al tuo presentViewController
dal 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
}
}
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;
}
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;
}
(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
/ AVPlayerViewController
e voglio fornire un semplice show(in view: UIView)
metodo che incorporare AVPlayerViewController
in view
. Per questo, ho bisogno di accesso view
's UIViewController
.
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.
È 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
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; \
})
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
}
}
sequence
bit). Quindi se "swifty" significa "più funzionale", allora suppongo che sia più veloce.
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)
}
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{
}
return
parola 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
}
}
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];
...
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;
EAGLViewController *vc = [(EAGLAppDelegate *)[UIApplication sharedApplication].delegate viewController];
.
ClassName *object
, con un asterisco.
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?
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.
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];
var parentViewController: UIViewController? {
let s = sequence(first: self) { $0.next }
return s.compactMap { $0 as? UIViewController }.first
}
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]];
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 ...