draw in swift does not respond to needsDisplay = true

I am developing a Mac app in Swift which does graphics animations in two windows. One is a Metal window (MTKView) which works well. The other one plots a graph as a subclass of NSView and is connected to the Metal window using an outlet (which works). When I want to update the second window, I use "graph.needsDisplay = true", where graph is the name of the outlet.

For some reason, the draw function does not execute when I use setNeedsDisplay in the MTKView class, whose draw command executes 60 times a second. I have verified that the draw function does work, since the graph is plotted when I resize the window, as it is supposed to do.

I have a version of this program (written using Obj. C and OpenGL ES) for iOS which I wrote about 5 years ago and it works fine. The draw function is the exact equivalent of the code below (in Obj. C):

class GraphView: NSView {
override func draw(_ dirtyRect:NSRect)
{
super.draw(dirtyRect)
var width:Float = 0.0, height:Float
let backgroundColor = NSColor.cyan
var iGraph, iData, i1:Int
var x,y:Int
backgroundColor.set()
NSBezierPath.fill(bounds)
height=Float(dirtyRect.size.height)
width=Float(dirtyRect.size.width)
let path=NSBezierPath()
NSColor.black.setStroke()
path.lineWidth = 0.5
path.move(to:NSPoint(x:0, y:Int(height)/2))
path.line(to:NSPoint(x:Int(width), y:Int(height)/2))
path.stroke()
current+=1
if current>200 {
current = 0 }
points[current]=jz
i1=current
x = 0
y = Int(0.25*height*(1.0-points[i1]/height_vector))
path.move(to:NSPoint(x:0, y:Int(y)))
for i in 0..<200
{
x = i*Int(width/200.0)
if i1<=0 {i1+=200}
y = Int(0.5*height - 0.45*height*points[i1]/height_vector)
path.line(to:NSPoint(x:x,y:y))
i1 -= 1
}
path.stroke()
}
}

Can anyone help? I have been trying everything under the sun and nothing works. I have been doing development of the Mac and iOS for more than 20 years, but, to be honest, have very little deep understanding of object oriented programming.

I submitted a query on a similar question which was answered about 2 days ago. Thanks in advance.

Could you show where you call the needsDisplay = true ?

And add a print statement to check if draw is called:

override func draw(_ dirtyRect:NSRect) {
super.draw(dirtyRect)
print("draw was called")

I agree with Claude that the first step is to verify that draw is called as a result of setting needsDisplay.

If so, there are some odd cases that you might need to investigate. For example, you might have ended up with two GraphViews, one on top of the other, and the update is being sent to the one you can't see. 🙂

Your drawing doesn't look correct to me though. You seem to be using dirtyRect as if it is the bounds of the view, but it's not that in general. It's only the part of the view that actually needs updating.

For example, it looks like you're intending to draw a horizontal guide line across the middle of the view, but this code will draw a line across the middle of the portion of the view that's being updated. It only works when the dirty rect is the whole view. Or am I misreading your code?

Thanks! I verified that draw is not called by putting in a breakpoint at some executable part of the draw code and it breaks once just after the views are loaded and never again. This gives it enough time to draw a few things - the cyan color, the line down the middle, etc.

In order to get it to draw the graph, I need to resize the whole window (which is mainly an MTKView with the GraphView on the bottom over part of the MTKView). The graph then draws more or less correctly, although it will be distorted by the irregular movements of my hand as I drag the corner of the window.

The Metal part of the display uses a GameViewController and a Renderer class which contains the Metal draw method. The latter is called at 60 fps and I put the needsDisplay in that method. It is linked to the GraphView draw by an outlet which appears in the GameViewController and connects to the GraphView in the StoryBoard.

OK, now things are very strange. Formerly, putting the outlet in GameViewController and the needsDisplay in the draw method in the (Metal) Renderer class (which is instantiated by the GameViewController class) worked fine (i.e., no complier complaints and nothing in the output panel in Xcode). Now, it gives me an out of scope error for the needsDisplay. I don't understand that change in behavior. Anyway, when it did work (no scope issues), I was able to get the graph to display by putting needsDisplay in one of the mouse functions connected to the GameViewController (moving the mouse gave me beautiful graphs!)

I tried to put the outlet in the Renderer class but the small circle never closed when I connected it to the GraphView in the StoryBoard. I also got messages in the output window that I needed an instance variable to link GameView to the MTKView view.

I am totally confused by all of this. Any suggestions welcome.

Thanks! I verified that draw is not called by putting in a breakpoint at some executable part of the draw code

You'd better put a print:

print("draw stared rect", dirtyRect)

And following @Polyphonic question, you could set a tag when you create the NSView and check the tag in the print That will give your more information.

print("draw stared rect", dirtyRect, "tag", self.tag)

I needed to use time machine to go back 2 days to get a version of the app which didn't complain about scope issues. So far as I know, this is identical to the version which did complain. Obviously, there is some subtle difference (maybe the GraphView in the StoryBoard was a little different?). I put both print statements at the beginning of the draw function. Here is what they give me:

draw stared rect (0.0, 0.0, 700.0, 102.0)

draw stared rect (0.0, 0.0, 700.0, 102.0) tag -1

By the way, I don't instantiate the GraphView class - should I do this? (I don't think that Hillegass' book on Cocoa does this either for his drawing example).

OK, the scope problem is back again. I need to get an outlet called "graph" in GameViewController to be recognized by a draw method in Renderer which is instantiated in GameViewController. It would be easier if I could put the outlet in the Renderer class, but Xcode won't allow this. Is there a way to force this?

It would be easier if I could put the outlet in the Renderer class, but Xcode won't allow this. Is there a way to force this?

If I understand correctly your question, answer in NO. IBOutlet must be declared in ViewController.

But you should be able to use delegation protocol to get access to the IBOutlet from the Renderer class

Thanks, that is what I am looking into. Despite using Obj. C and Swift a fair amount, there is a lot I don't know about - for some reason, object oriented programming never made much sense to me. (As a retired physicist, I was brought up in Fortran, and APL, which I loved).

Just a quick question: I can make a public method in the GameViewController, as follows:

IBOutlet weak var graph:GraphView!
public func viewData(){
graph.needsDisplay = true
}

where the outlet is connected to the GraphView rect in the StoryBoard. If I try to refer to this method from a Renderer class method, I get "cannot find viewData in scope". I am just using a line such as:

viewData()

in the Renderer method. Do you know why this happens? Do I need to call the method in some other way, since it is in a different class?

Thanks!

If I try to refer to this method from a Renderer class method,

viewData() is defined in GameViewController. It is not visible directly outside, in Renderer.

But if you had defined (I don't say you need to do)

var gameViewController = GameViewController()

in Renderer

then you can call

gameViewController.viewData() 

You could also use delegation pattern to achieve something similar.

I borrowed the Metal part from a tutorial on the web - it drew a 3D box with lighting, etc. I simply replaced the vertices with the much more complicated structures I wanted to draw and included mouse interaction so one could rotate the objects. This all worked well.

It turns out the the author included an instance of the GameViewController in the Renderer class:

   public var graphRef = GameViewController()

My code at the beginning of the GameViewController class is:

@IBOutlet weak var graph: GraphView!
func dodraw(){
graph.needsDisplay = true
}

In the draw method from Renderer I invoke dodraw using:

graphRef.dodraw()

When I run this, I get a message: Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value. If I include a test for nil in dodraw(), I am back to where I was: graph is always nil. The outlet seemed OK - it was the only one connecting the GameViewClass to the GraphView rect and the circle was filled in.

Any ideas?

draw in swift does not respond to needsDisplay = true
 
 
Q