Continuously Redrawing a Path with Updated Data

189 views Asked by At

I am developing an audio visualizer MacOS app, and I want to use Quartz/CoreGraphics to render the time-varying spectrum coordinated with the playing audio. My Renderer code is:

import Cocoa

class Renderer: NSView {

override func draw(_ dirtyRect: NSRect) {
    super.draw(dirtyRect)
    NSColor.white.setFill()
    bounds.fill()
    
    guard let context = NSGraphicsContext.current?.cgContext else {return}

    var x : CGFloat = 0.0
    var y : CGFloat = 0.0

    context.beginPath()
    context.move(to: CGPoint(x: x, y: y))

    for bin in 0 ..< 300 {
        x = CGFloat(bin)
        y = CGFloat(Global.spectrum[bin])
        context.addLine(to: CGPoint(x: x, y: y))
    }
    
    context.setStrokeColor(CGColor( red: 1, green: 0, blue: 0, alpha: 1))
    context.setLineWidth(1.0)
    context.strokePath()

    self.setNeedsDisplay(dirtyRect)
}

}

This draws the path once - using the initial all-zeroes values of the spectrum[] array - and then continues to draw that same all-zeroes line indefinitely. It does not update using the new values in the spectrum[] array. I used a print() statement to verify that the values themselves are being updated, but the draw function does not redraw the path using the updated spectrum values. What am I doing wrong?

1

There are 1 answers

2
apodidae On BEST ANSWER

The following demo shows how to update an NSView with random numbers created by a timer in a separate class to hopefully mimic your project. It may be run in Xcode by setting up a Swift project for MacOS, copy/pasting the source code into a new file called 'main.swift', and deleting the AppDelegate supplied by Apple. A draw function similar to what you posted is used.

import Cocoa

var view : NSView!
var data = [Int]()

public extension Array where Element == Int {
    static func generateRandom(size: Int) -> [Int] {
        guard size > 0 else {
            return [Int]()
        }
        return Array(0..<size).shuffled()
    }
}

class DataManager: NSObject {
var timer:Timer!

@objc func fireTimer() {
data = Array.generateRandom(size:500)
view.needsDisplay = true
}

func startTimer(){
timer = Timer.scheduledTimer(timeInterval: 2.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)
}

func stopTimer() {
 timer?.invalidate()
}

}
let dataMgr = DataManager()

class View: NSView {

override func draw(_ rect: NSRect) {
 super.draw(rect)
 NSColor.white.setFill()
 bounds.fill()
    
 guard let gc = NSGraphicsContext.current?.cgContext else {return}

  var xOld : CGFloat = 0.0
  var yOld : CGFloat = 0.0
  var xNew : CGFloat = 0.0
  var yNew : CGFloat = 0.0
  var counter : Int = 0

  gc.beginPath()
  gc.move(to: CGPoint(x: xOld, y: yOld))

  for i in 0 ..< data.count {
    xNew = CGFloat(counter)
    yNew = CGFloat(data[i])
    gc.addLine(to: CGPoint(x: xNew, y: yNew))
    xOld = xNew;
    yOld = yNew;
    counter = counter + 1
  }
    
  gc.setStrokeColor(CGColor( red: 1, green: 0, blue: 0, alpha: 1))
  gc.setLineWidth(1.0)
  gc.strokePath()
}

}

class ApplicationDelegate: NSObject, NSApplicationDelegate {
 var window: NSWindow!

@objc func myStartAction(_ sender:AnyObject ) {
  dataMgr.startTimer()
}

@objc func myStopAction(_ sender:AnyObject ) {
  dataMgr.stopTimer()
}

func buildMenu() {
let mainMenu = NSMenu()
 NSApp.mainMenu = mainMenu
 // **** App menu **** //
 let appMenuItem = NSMenuItem()
 mainMenu.addItem(appMenuItem)
 let appMenu = NSMenu()
 appMenuItem.submenu = appMenu
 appMenu.addItem(withTitle: "Quit", action:#selector(NSApplication.terminate), keyEquivalent: "q") 
}

func buildWnd() {

data = Array.generateRandom(size: 500)

 let _wndW : CGFloat = 800
 let _wndH : CGFloat = 600

 window = NSWindow(contentRect: NSMakeRect( 0, 0, _wndW, _wndH ), styleMask:[.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false)
 window.center()
 window.title = "Swift Test Window"
 window.makeKeyAndOrderFront(window)

// **** Start Button **** //
 let startBtn = NSButton (frame:NSMakeRect( 30, 20, 95, 30 ))
 startBtn.bezelStyle = .rounded
 startBtn.title = "Start"
 startBtn.action = #selector(self.myStartAction(_:))
 window.contentView!.addSubview (startBtn)

// **** Stop Button **** //
 let stopBtn = NSButton (frame:NSMakeRect( 230, 20, 95, 30 ))
 stopBtn.bezelStyle = .rounded
 stopBtn.title = "Stop"
 stopBtn.action = #selector(self.myStopAction(_:))
 window.contentView!.addSubview (stopBtn)

// **** Custom view **** //
 view = View( frame:NSMakeRect(20, 60, _wndW - 40, _wndH - 80)) 
 view.autoresizingMask = [.width, .height]      
 window.contentView!.addSubview (view)
    
// **** Quit btn **** //
 let quitBtn = NSButton (frame:NSMakeRect( _wndW - 50, 10, 40, 40 ))
 quitBtn.bezelStyle = .circular
 quitBtn.autoresizingMask = [.minXMargin,.maxYMargin]
 quitBtn.title = "Q"
 quitBtn.action = #selector(NSApplication.terminate)
 window.contentView!.addSubview(quitBtn)
}
 
func applicationDidFinishLaunching(_ notification: Notification) {
 buildMenu()
 buildWnd()
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
 return true
}

}
let applicationDelegate = ApplicationDelegate()

// **** main.swift **** //
let application = NSApplication.shared
application.setActivationPolicy(NSApplication.ActivationPolicy.regular)
application.delegate = applicationDelegate
application.activate(ignoringOtherApps:true)
application.run()