How to inspect the responder chain?

2020-02-02 12:27发布

问题:

I'm doing some crazy multiple documents inside a single window stuff with the document-based architecture and I'm 95% done.

I have this two-tier document architecture, where a parent document opens and configures the window, providing a list of "child" documents. When the user selects one of the children, that document is opened with the same window controller and it places a NSTextView in the window. The window controller's document association is changed so that the "edited dot" and the window title track the currently selected document. Think of an Xcode project and what happens when you edit different files in it.

To put the code in pseudo form, a method like this is invoked in the parent document when a child document is opened.

-(void)openChildDocumentWithURL:(NSURL *)documentURL {
  // Don't open the same document multiple times
  NSDocument *childDocument = [documentMapTable objectForKey:documentURL];
  if (childDocument == nil) {
    childDocument = [[[MyDocument alloc] init] autorelease];
    // Use the same window controller
    // (not as bad as it looks, AppKit swaps the window's document association for us)
    [childDocument addWindowController:myWindowController];
    [childDocument readFromURL:documentURL ofType:@"Whatever" error:NULL];

    // Cache the document
    [documentMapTable setObject:childDocument forKey:documentURL];
  }

  // Make sure the window controller gets the document-association swapped if the doc came from our cache
  [myWindowController setDocument:childDocument];

  // Swap the text views in
  NSTextView *currentTextView = myCurrentTextView;
  NSTextView *newTextView = [childDocument textView];
  [newTextView setFrame:[currentTextView frame]]; // Don't flicker      

  [splitView replaceSubview:currentTextView with:newTextView];

  if (currentTextView != newTextView) {
    [currentTextView release];
    currentTextView = [newTextView retain];
  }
}

This works, and I know the window controller has the correct document association at any given time since the change dot and title follow whichever document I'm editing.

However, when I hit save, (CMD+S, or File -> Save/Save As) it wants to save the parent document, not the current document (as reported by [[NSDocumentController sharedDocumentController] currentDocument] and as indicated by the window title and change dot).

From reading the NSResponder documentation, it seems like the chain should be this:

Current View -> Superview (repeat) -> Window -> WindowController -> Document -> DocumentController -> Application.

I'm unsure how the document based architecture is setting up the responder chain (i.e. how it's placing NSDocument and NSDocumentController into the chain) so I'd like to debug it, but I'm not sure where to look. How do I access the responder chain at any given time?

回答1:

You can iterate over the responder chain using the nextResponder method of NSResponder. For your example, you should be able to start with the current view, and then repeatedly print out the result of calling it in a loop like this:

NSResponder *responder = currentView;
while ((responder = [responder nextResponder])) {
     NSLog(@"%@", responder);
}


回答2:

Here is another version for Swift users:

func printResponderChain(_ responder: UIResponder?) {
    guard let responder = responder else { return; }

    print(responder)
    printResponderChain(responder.next)
}

Simply call it with self to print out the responder chain starting from self.

printResponderChain(self)


回答3:

I'll improve a bit on the Responder category answer, by using a class method which feels more "useable" when debugging (you don't need to break in a specific view or whatever).

Code is for Cocoa but should be easily portable to UIKit.

@interface NSResponder (Inspect)

+ (void)inspectResponderChain;

@end

@implementation NSResponder (Inspect)

+ (void)inspectResponderChain
{
  NSWindow *mainWindow = [NSApplication sharedApplication].mainWindow;

  NSLog(@"Responder chain:");
  NSResponder *responder = mainWindow.firstResponder;
  do
  {
    NSLog(@"\t%@", [responder debugDescription]);
  }
  while ((responder = [responder nextResponder]));
}

@end


回答4:

You can also add a category to class UIResponder with appropriate method that is possible to be used by any subclass of UIResponder.

@interface UIResponder (Inspect)

- (void)inspectResponderChain; // show responder chain including self

@end

@implementation UIResponder (Inspect)

- (void)inspectResponderChain  
{
    UIResponder *x = self;
    do {
        NSLog(@"%@", x);
    }while ((x = [x nextResponder]));
}
@end

Than you can use this method somewhere in code as the example below:

- (void)viewDidLoad {
    ...
    UIView *myView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    [self.view addSubview:myView];
    [myView inspectResponderChain]; // UIView is a subclass of UIResponder
    ...
}


回答5:

Swift:

extension UIResponder {
    var responderChain: [UIResponder] {
        var chain = [UIResponder]()
        var nextResponder = next
        while nextResponder != nil {
            chain.append(nextResponder!)
            nextResponder = nextResponder?.next
        }
        return chain
    }
}

// ...

print(self.responderChain)


回答6:

Here is the simplest one

    extension UIResponder {
        func responderChain() -> String {
            guard let next = next else {
                return String(describing: self)
            }

            return String(describing: self) + " -> " + next.responderChain()
        }
    }

    // ...

    print(self.responderChain())