need help troubleshooting an NSResponder subclass

hey, completely frustrated here. I just can't get it underway, and troubleshooting docs do not exist, so any help would be appreciated.


what i am doing:

I'm writing a control class for onscreen elements, that is a subclass of NSResponder. And then the intention is to insert my class into the responder chain right after the ViewController. Ideally, the ViewController will get mouse events from the view (confirmed), and then pass them to my subclass up the responder chain (this is where things go off the rails.)


things I know that are going to be asked:

does my view class accept firstResponder : yes.

class firstResponderView : NSView{
    override var acceptsFirstResponder: Bool{
        get{
            return true
        }
    }
}


does my subclass accept first responder : yes.

override var acceptsFirstResponder : Bool {
        get{
            return true
        }
    }



do I add my subclass to the responder chain? i try to. in the debugger i get no indication that my setting the property in the ViewController, does anything. the NextResponder property is populated, but it's not my subclass.

currently in the ViewController's viewDidLoad() method:

self.nextResponder = (quadManip as NSResponder)


previously in my NSResponder subclass, when I thought setting the NSResponder was going to be fairly straight forward:

    func joinResponderChain(_ responder : NSResponder){
        responderParent = responder // this is a class property, so that I can put things back the way I found them, later.
        let temp = responderParent?.nextResponder
        responderParent?.nextResponder = self
        self.nextResponder = temp
    }



results: when my viewController gets the mouseDown event, I have written an override so that I can see what's going on :

override func mouseDown(with event: NSEvent) {
        if let next = self.nextResponder{
            next.mouseDown(with: event)
        }
    }


and there is definetly a nextResponder, but it is not the one I have set. it has never been the one I have set, and my subclass's mouse handling methods are never called.

Replies

I think I found out what is happening.


some almost viral code was written that steam rolls anything you do with the responder chain, prior to the view being inserted into the window.


which means: don't try to insert your NSResponder subclasses into the responder chain in : viewDidLoad()

do it instead in : viewDidAppear() in your viewController class.

because why not?

It's not clear what your actual code is, but the single line of code that you say you put in the view controller's viewDidLoad:


self.nextResponder = (quadManip as NSResponder)


cannot possibly be correct, because it overwrites the current value of the view controller's nextResponder property. (Actually, it doesn't, because of the other problem you have, but that line of code is still wrong.)


You have to keep the concept of the responder chain very carefully in mind. First of all, there is no single responder chain. The "nextResponder" property of the various responder objects defines a tree whose root is the application. The leaves of the tree are (typically but not necessarily) all of your views. Starting from any one leaf view, there is a unique responder chain up to the root. The responder chain is the chain that starts from the current first responder view. IOW, a different responder chain can become the responder chain at any time. In practice, we also talk about the responder chain when we have a particular leaf view in mind, even if it's not currently first responder, in order to understand the upward (towards the root) flow of event messages if it were first responder.


But that's not all. The tree of responders changes over time. A view is not in the responder tree until it's added to the view hierarchy, which is to say, when the view is added to the window. The view controller is added to the responder tree when its view added.


That's why, as you discovered, viewDidLoad is the wrong time to change the responder chain. The view controller's view is not being added to the window at that time. It's added a bit later, just after viewWillAppear is invoked and just before viewDidAppear is invoked. That means viewDidAppear is the correct place to manipulate "the responder chain" (i.e manipulate the responder tree) if you're putting the code in the view controller.


However, I strongly recommend you avoid doing this. If you insert something into the responder chain, you have to be very careful to remove it safely and at a suitable time, and it can be subtle to determine the correct time to do this. I strongly recommend that you avoid messing with the responder chain at all. It would be far safer to capture the events in the view controller, and forward the appropriate ones to your support class instance, for which you keep a simple reference in the view controller. That is, after all, what your projected solution would do: anything that the view controller doesn't handle, the support class does or can.


Lastly, if neither the view controller or the support class wants to handle a particular event, but you have an event method override to capture that event type and to make the decision, you should forward unhandled events up the event chain, from the view controller override, by the convention of invoking super. That's the documented way of saying "I don't want to handle this event."

Thanks for the response QuincyMorris,


I'm curious though, about your advice regarding staying away from the responder chain altogether. I understand that many aspects of the OS that used to be well documented and transparent, are now opaque (in more ways than one,) But the responder chain is an Opt-in technology. Until Apple comes down from the cloud and says " this is broken" it should work like it always worked.


I'm sorry that I didn't provide enough context. It's difficult to editorialize to the right amount of context with these issues, and I try to keep it as simple as possible.

the code example of : "self.nextResponder = (quadManip as NSResponder) " was what I did to try to get some leverage and understanding versus my expectations. the code following that:

func joinResponderChain(_ responder : NSResponder){
        responderParent = responder
       
        let temp = responderParent?.nextResponder
        responderParent?.nextResponder = self
        self.nextResponder = temp
    }


is the real code (in use now that I have determined the correct moment, to call it.)


I have written countless event handling method forwarding systems. small ones, big ones, and deep ones. I knew that there was an easier way to do it, but I just never saw any point in supporting the responder chain. Too much of a learning curve on the front end. I finally get around to it, and you're saying what? That the responder chain changes in unpredictable ways? I'm sorry, that just isn't so. Look, the problem with when to hook up different objects as the app is loading has been solved more than once over the years: viewDidLoad, awakefromNib. there's methods out there that specifically address those moments. You're supposed to use as much of Apple's code as you possibly can... in part to lower the amount of work you have to do, but also to allow your work benefit from advances that Apple provides. There's one method that is called at the point that you should be inserting objects into the responder chain. And the chain itself is very easy to comprehend. It's one of the older technologies in the OS, and it's pretty elegant. I just don't see any reason not to join the responder chain. you get in After the view is added to the window, and get out before the view is removed from the window.


I wouldn't be doing it this way if there wasn't a need for some serious amounts of event handling in a single view. lots of widgets, in a hierarchy, and many of them with customized behavior. I appreciate the heads up, but unless the Responder chain has been marked private, or deprecated, I don't see any reason not to use it in my app. jeez, the next best thing is to build my own private responder chain (which I would rather avoid.)


it's unlikely that in my app, my inserted responder object will ever hang around long enough to run afoul of the subtle timing for removing objects from the responder tree, But I will pay attention to it, and try to learn what is correct.


the thing that clued me in to the solution in the first place was creating an irrevocable loop in the responder chain. I got a nice, descriptive error, that also hinted at the status of some of the objects... and the window was 0x0. Sometimes you have to break stuff on purpose, just to find the edges.

It's fine if you want to invest the effort to ensure everything works right. Your understanding of how to use the augmented responder chain is correct, and the problem is about timing and memory management. If you look at the declaration of the nextResponder property:


https://developer.apple.com/documentation/appkit/nsresponder/1528245-nextresponder


you'll see that it's declared "unowned(unsafe)". That's Swift's way of washing its hands of responsibility for memory management. In theory, if you insert an object in viewDidAppear, you will remove it in viewWillDisappear. But are you sure the latter is called under all circumstances? (I have seen reports that it is not called under some specific conditions.) At some point you must patch the extra object out of the responder chain, but what if the view controller is deallocated due to the last owning reference, wherever that is, going away? What assumptions, if any, can you make about deallocation order, and to what extent do they matter? What if something else inserts an object into the responder chain between your view controller and your additional responder?


There are ways to deal with all this, since developers have been (occasionally) inserting things into the responder chain for a long time. As you say, it's one of the older technologies in macOS (and it may actually be inherited from NeXTStep, which makes it even older), but it doesn't really meet current standards for safety and usability. If you want to go with it, then go ahead.


IMO, for all practical purposes, the only good reason for manipulating the responder chain used to be to insert a view controller into it. However, starting from macOS 10.10, this now happens automatically (and it's Apple's job to handle all the edge cases). So, I don't envisage ever having to open this particular can of worms ever again.

so... here's where I wound up :

I added a property to my ViewController : localResponder : NSResponder? = nil


then I just make my own private responder chain off of it. I've put some time into working with it, and I think it's the best of all worlds.

I get the benefits of the respnder chain (less code, more flexibility)

and I'm not mucking around in semi-private, behind the scenes stuff.


here's how I handle events:

    override func mouseDown(with event: NSEvent) {
        if self.view.bounds.contains(self.view.convert(event.locationInWindow, from: nil)){
            wasHit = true
            localResponder?.mouseDown(with: event)
        }else{
            super.mouseDown(with: event)
        }
    }

    override func mouseDragged(with event: NSEvent) {
        if wasHit{
            localResponder?.mouseDragged(with: event)
        }else{
            super.mouseDragged(with: event)
        }
    }

    override func mouseUp(with event: NSEvent) {
        if wasHit{
            localResponder?.mouseUp(with: event)
            wasHit = false
        }else{
            super.mouseUp(with: event)
        }
    }


everything looked pretty well put together.

which leads me to my very next question. (which I have to assume is related to NSResponder)


and this is quite wierd. wasHit, which is a property defined thusly: var wasHit : Bool = false

is used extensively in my class hierarchies in the event handling methods.


if the mouse hits my control surface on mouse down, I set 'wasHit' to true.

then in the mouseDragged method, I do not test if the control surface is hit by the mouse... I just test for "wasHit == true"

because we don't care if the mouse is on the control now.


in any case, I get false negatives for that test.

it was really difficult to track down, but I finally did. and the results are : wasHit will occasionally stop evaluating to it's value, and instead will evaluate to a value that has not been set.


I determined this by first putting an NSLog in the section that runs if wasHit = true, it seemed that my event handling method was not being called.

I tracked back through all the classes looking for code issues, and when I ran out of possibilities, I put another NSLog outside the 'wasHit' test.


    override func mouseDragged(with event: NSEvent){
        NSLog("method Called")
        if wasHit{
            NSLog("isHit evaluates to true")
            let locPos = event.locationInWindow
            let newDiff = locPos - mousePos
            let pixValue : CGFloat = newDiff.x
            let range = (data!.hiRange - data!.lowRange)
            let dur : CGFloat = (ruler!.scroller!.rev - ruler!.scroller!.rsv) * range
            var aVal = dur / self.layer.superlayer!.bounds.width * pixValue
            aVal = round(aVal * 100)/100
   
            if abs(aVal) > 0.01{
                self.data?.progress = basePos + aVal
                self.updateManip()
            }
        }else{
            super.mouseDragged(with: event)
        }
    }



there is one single place in all of my code that sets the 'wasHit' property back to 'false'

so of course, I set an NSLog at this point.


override func mouseUp(with event: NSEvent){
        if wasHit{
            NSLog("wasHit reset")
            wasHit = false
            mouseUp()
        }else{
            super.mouseUp(with: event)
        }
    }


and I have an instance... in Swift 4, where the value of a variable is not properly evaluated.

I mean, sure... maybe there's operator error, but this looks like Swift is playing fast and loose with something fairly important: the value of a Boolean.


have you ever heard of anything like this? I know I've seen things like it before, But I assumed it was me, and I did not properly document it.


* I should probably mention that this occurs during mouse event handling methods, specifically the mouseDragged method, and it happens when the mouse is moving at a good clip. Hence having to resort to NSLog as a diagnostic tool. you have to put the system under stress, and dropping into the debugger instantly puts a stop to the stress.

I don't know. Your description is a bit difficult to evaluate, because you're talking about evolutions of code, it's hard to keep track of what's going on.


>> in Swift 4, where the value of a variable is not properly evaluated


I think you're a ways away from that conclusion. Apart from anything else, if Swift was doing this wrong with such (syntactically) simple code, the error would have shown up elsewhere. You haven't verified, for example, that all of the log messages come from the same instance of the view controller.


There are also some other troubling oddities in your code:


override func mouseUp(with event: NSEvent){
        if wasHit{
            NSLog("wasHit reset")
            wasHit = false
            mouseUp()
            …


you're calling a different "mouseUp", but we can't see what's in it. You also use this pattern several times:


    override func mouseDown(with event: NSEvent) { 
        if self.view.bounds.contains(self.view.convert(event.locationInWindow, from: nil)){ 
            wasHit = true 
            localResponder?.mouseDown(with: event) 
        }else{ 
            super.mouseDown(with: event) 
        } 
    }


If "wasHit" is true, then you conditionally invoke the localResponder event handler. However, if localResponder is nil, you don't invoke super, which means the event is just discarded. This may be what you intend, but it's not obvious what your intent is. For that matter, in this particular case, don't you already know that the location is inside the view, otherwise the mouseDown event would have gone to a different view? Also, in your second-to-last code fragment, for mouseDragged, you don't try to invoke localResponder.mouseDragged at all, which contrdicts the earlier mouseDragged code fragment.


I don't see anything in all this that explains the apparent problem, but it's not clear enough what is going on. I think maybe you need to pick one version of the code, log all the paths through the code, not just some of them, and double-check that you're only seeing the instance variable values of one instance.