Search code examples
macoscocoacore-graphicsquartz-2dsetneedsdisplay

Continuously Redrawing a Path with Updated Data


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?


Solution

  • 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()