2014-11-04 6 views
8

Gli storyboard per le app Cocoa sembrano un'ottima soluzione in quanto preferisco la metodologia che si trova in iOS. Tuttavia, mentre rompere le cose in controller di vista separati rende molto logico, non sono chiaro su come passare il controllo della finestra (pulsanti della barra degli strumenti) o l'interazione del menu fino ai controller di vista che si preoccupano. Il delegato della mia app è il primo risponditore e riceve le azioni del menu o della barra degli strumenti, tuttavia, come posso accedere al controller della vista di cui ho bisogno per ottenere quel messaggio? Puoi semplicemente visualizzare in dettaglio la gerarchia dei controller di visualizzazione. In tal caso, come ci si arriva dal delegato dell'app poiché è il primo risponditore? Puoi invece rendere il controller della finestra il primo soccorritore. Se é cosi, come? Nello storyboard? Dove?Catena di risposta per storyboard di cacao

Poiché questa è una domanda di alto livello, potrebbe non avere importanza, tuttavia, sto usando Swift per questo progetto se vi state chiedendo.

risposta

5

Non sono sicuro che sia presente un metodo "corretto" per risolvere questo problema, tuttavia, ho trovato una soluzione che utilizzerò per ora. Prima un paio di dettagli

  • La mia app è un'applicazione basata su documenti in modo che ogni finestra ha un'istanza del documento.

  • Il documento gli usi app possono agire come primo responder e in avanti qualunque azione che ho collegato

  • Il documento è in grado di ottenere una sospensione del controller superiore finestra di livello e da lì sono in grado di analizzare la gerarchia del controller di visualizzazione per ottenere il controller di visualizzazione di cui ho bisogno.

Quindi, a mio windowDidLoad sul controller della finestra, faccio questo:

override func windowDidLoad() { 
    super.windowDidLoad() 

    if self.contentViewController != nil { 
     var vc = self.contentViewController! as NSSplitViewController 
     var innerSplitView = vc.splitViewItems[0] as NSSplitViewItem 
     var innerSplitViewController = innerSplitView.viewController as NSSplitViewController 
     var layerCanvasSplitViewItem = innerSplitViewController.splitViewItems[1] as NSSplitViewItem 
     self.layerCanvasViewController = layerCanvasSplitViewItem.viewController as LayerCanvasViewController 
    } 
} 

Il che mi ottiene il controller della vista (che controlla la vista che si vede delineato in rosso sotto) e imposta un locale proprietà nel controller della finestra.

enter image description here

Così ora, posso inoltrare gli eventi dei pulsanti della barra degli strumenti o voce di menu direttamente nella classe di documento che si trova nella catena risponditore e riceve quindi le azioni che l'installazione nelle voci di menu e barre degli strumenti. Come questo:

class LayerDocument: NSDocument { 

    @IBAction func addLayer(sender:AnyObject) { 
     var windowController = self.windowControllers[0] as MainWindowController 
     windowController.layerCanvasViewController.addLayer() 
    } 

    // ... etc. 
} 

Dal momento che la LayerCanvasViewController è stato impostato come una proprietà del controller finestra principale quando è stata caricata, posso solo accedervi e chiamare i metodi di cui ho bisogno.

1

Ho avuto lo stesso problema con Storyboard ma con una singola finestra senza documenti. È una porta di un'app per iOS e la mia prima app per OS X. Ecco la mia soluzione.

Per prima cosa aggiungi un IBAction come hai fatto in precedenza in LayerDocument. Ora vai su Interface Builder. Vedrai che nel pannello delle connessioni a First Responder nel tuo WindowController, IB ha ora aggiunto un'azione Sent di addLayer. Collega il tuo ToolBarItem a questo. (Se si guardano le connessioni First Responder per qualsiasi altro controller, avrà un'azione Received di addLayer. Non potrei fare nulla con questo.)

Torna alla finestraDidLoad. Aggiungi le seguenti due righe.

// This is the top view that is shown by the window 

NSView *contentView = self.window.contentView; 

// This forces the responder chain to start in the content view 
// instead of simply going up to the chain to the AppDelegate. 

[self.window makeFirstResponder: contentView]; 

Che dovrebbe farlo.Ora quando fai clic sulla barra degli strumenti, questo andrà direttamente alla tua azione.

+2

Questo funziona solo per la prima volta. Quando il primo risponditore si trasforma in un oggetto che non ha l'azione 'addLayer' nella sua catena, l'azione non verrà chiamata. Ad esempio, questo è il caso quando si ha uno SplitViewController e uno di questi è selezionato. Gli altri elementi non sono più nella catena dell'elemento selezionato. – JanApotheker

2

Per l'azione di trovare i controller di vista, è necessario implementare -supplementalTargetForAction: mittente: nei tuoi controller finestra e vista.

Si potrebbero elencare tutti i controller bambino potenzialmente interessati nell'azione, o utilizzare un'implementazione generica:

- (id)supplementalTargetForAction:(SEL)action sender:(id)sender 
{ 
    id target = [super supplementalTargetForAction:action sender:sender]; 

    if (target != nil) { 
     return target; 
    } 

    for (NSViewController *childViewController in self.childViewControllers) { 
     target = [NSApp targetForAction:action to:childViewController from:sender]; 

     if (![target respondsToSelector:action]) { 
      target = [target supplementalTargetForAction:action sender:sender]; 
     } 

     if ([target respondsToSelector:action]) { 
      return target; 
     } 
    } 

    return nil; 
} 
1

ho lottato con questa domanda me stesso.

Penso che la risposta 'giusta' è quello di appoggiarsi sulla catena responder. Ad esempio, per connettere un'azione dell'elemento della barra degli strumenti, è possibile selezionare il primo risponditore del controller della finestra radice. E poi mostra l'ispettore degli attributi. Nell'ispettore attributi, aggiungi la tua azione personalizzata (vedi foto).

Creating custom responder action

quindi collegare il prodotto barra degli strumenti per l'azione. (Controlla il trascinamento dalla tua barra degli strumenti al primo risponditore e seleziona l'azione appena aggiunta.)

Infine, puoi andare al ViewController (+ 10.10) o ad altro oggetto, purché sia ​​presente nella catena del risponditore, dove si desidera ricevere questo evento e aggiungere il gestore.

In alternativa, invece di definire l'azione nel attributi di ispezione. Puoi semplicemente scrivere IBAction nel tuo ViewController. Poi, vai alla voce della barra degli strumenti, e controllare trascinare per first responder del controller finestra - e selezionare l'IBAction appena aggiunto. L'evento viaggerà quindi attraverso la catena di risposta fino a quando non verrà ricevuto dal controller della vista.

Penso che questo sia il modo corretto per farlo senza introdurre alcun ulteriore accoppiamento tra i controller e/o l'inoltro manuale della chiamata.

L'unica sfida in cui mi sono imbattuto, essendo nuova a Mac Dev me stesso, a volte è stata disattivata automaticamente dalla barra degli strumenti dopo aver ricevuto il primo evento. Quindi, mentre penso che questo sia l'approccio corretto, ci sono ancora alcuni problemi che ho incontrato.

Ma sono in grado di ricevere l'evento in un'altra posizione senza alcun aggancio o ginnastica aggiuntiva.

0

Come io sono una persona molto pigra mi si avvicinò con la seguente soluzione basata su Pierre Bernard s' versione

#include <objc/runtime.h> 
//----------------------------------------------------------------------------------------------------------- 

IMP classSwizzleMethod(Class cls, Method method, IMP newImp) 
{ 
    auto methodReplacer = class_replaceMethod; 
    auto methodSetter = method_setImplementation; 

    IMP originalImpl = methodReplacer(cls, method_getName(method), newImp, method_getTypeEncoding(method)); 

    if (originalImpl == nil) 
     originalImpl = methodSetter(method, newImp); 

    return originalImpl; 
} 
// ---------------------------------------------------------------------------- 

@interface NSResponder (Utils) 
@end 
//------------------------------------------------------------------------------ 

@implementation NSResponder (Utils) 
//------------------------------------------------------------------------------ 

static IMP originalSupplementalTargetForActionSender; 
//------------------------------------------------------------------------------ 

static id newSupplementalTargetForActionSenderImp(id self, SEL _cmd, SEL action, id sender) 
{ 
    assert([NSStringFromSelector(_cmd) isEqualToString:@"supplementalTargetForAction:sender:"]); 

    if ([self isKindOfClass:[NSWindowController class]] || [self isKindOfClass:[NSViewController class]]) { 
     id target = ((id(*)(id, SEL, SEL, id)) originalSupplementalTargetForActionSender)(self, _cmd, action, sender); 

     if (target != nil) 
      return target; 

     id childViewControllers = nil; 

     if ([self isKindOfClass:[NSWindowController class]]) 
      childViewControllers = [[(NSWindowController*) self contentViewController] childViewControllers]; 
     if ([self isKindOfClass:[NSViewController class]]) 
      childViewControllers = [(NSViewController*) self childViewControllers]; 

     for (NSViewController *childViewController in childViewControllers) { 
      target = [NSApp targetForAction:action to:childViewController from:sender]; 

      if (NO == [target respondsToSelector:action]) 
       target = [target supplementalTargetForAction:action sender:sender]; 

      if ([target respondsToSelector:action]) 
       return target; 
     } 
    } 
    return nil; 
} 
// ---------------------------------------------------------------------------- 

+ (void) load 
{ 
    Method m = nil; 

    m = class_getInstanceMethod([NSResponder class], NSSelectorFromString(@"supplementalTargetForAction:sender:")); 
    originalSupplementalTargetForActionSender = classSwizzleMethod([self class], m, (IMP)newSupplementalTargetForActionSenderImp); 
} 
// ---------------------------------------------------------------------------- 

@end 
//------------------------------------------------------------------------------ 

In questo modo non c'è bisogno di aggiungere il codice spedizioniere al controller finestra e tutto le viewcontrollers (anche se sottoclassi farebbe che un po 'più facile), la magia accade automaticamente se si dispone di un viewcontroller per la finestra contentView.

Swizzling è sempre un po 'pericoloso, quindi non è una soluzione perfetta, ma l'ho provato con una gerarchia di visualizzazione/viewcontroller molto complessa che, utilizzando le visualizzazioni container, ha funzionato correttamente.