Home > Blockchain >  Dynamically create and remove NSWindow
Dynamically create and remove NSWindow

Time:02-05

I'd like to create (and destroy) NSWindows programmatically

class Wins: NSObject, NSWindowDelegate {

    var windows = Set<NSWindow>()

    func createWindow() {
        
        let newWindow = NSWindow(contentRect: .init(origin: .zero, size: .init(width: 300, height: 300)),
                                 styleMask: NSWindow.StyleMask(rawValue: 0xf),
                                 backing: .buffered,
                                 defer: false)
        newWindow.title = "New Window"
        newWindow.isOpaque = false
        newWindow.isMovableByWindowBackground = true

        newWindow.backgroundColor = NSColor(calibratedHue: 0, saturation: 1.0, brightness: 0, alpha: 0.7)
        newWindow.makeKeyAndOrderFront(nil)

        let windowController = NSWindowController()
        windowController.window = newWindow

        windows.insert(newWindow)
    }

    func closeAll() {
        for win in windows {
            windows.remove(win)
            win.close()
        }
    }

}
 

Although the code above works, the closed windows are never deallocated and keep piling up in the memory.

If I remove the windowController assignment, the app crashes when I try to close the windows with EXC_BAD_ACCESS, and using the profiler, I can see the window was actually deallocated:

An Objective-C message was sent to a deallocated 'NSWindow' object (zombie) at address...

So I believe the windowController instance is the culprit and is never being destroyed.

How can I implement a proper NSWindow lifecycle for programmatically created windows?

CodePudding user response:

Notice the docs says for the property isReleasedWhenClosed

Release when closed, however, is ignored for windows owned by window controllers

You are correct in saying that the windowController instance is the culprit. Even when the NSWindwow is closed, there is a strong reference to NSWindowController. To allow the NSWindow to deinit, you should set the controller to nil and then close the window. So something like

func closeAll() {
    for win in windows {
        windows.remove(win)
        win.contentViewController = nil
        win.close()
    }
}

would work, or you could implement the solution recommended by Apple to "have its delegate autorelease it on receiving a windowShouldClose(_:) message", so add a function

func windowShouldClose(_ sender: NSWindow) -> Bool {
    sender.contentViewController = nil
    return true
}

to your class (which I assume is the NSWindowDelegate for this window)

CodePudding user response:

The deallocation of NSWindow is a bit complicated for historical reasons. From the documentation of NSWindow.close():

If the window is set to be released when closed, a release message is sent to the object after the current event is completed. For an NSWindow object, the default is to be released on closing, while for an NSPanel object, the default is not to be released. You can use the isReleasedWhenClosed property to change the default behavior.

From the documentation of isReleasedWhenClosed:

The value of this property is true if the window is automatically released after being closed; false if it’s simply removed from the screen.

The default for NSWindow is true; the default for NSPanel is false. Release when closed, however, is ignored for windows owned by window controllers. Another strategy for releasing an NSWindow object is to have its delegate autorelease it on receiving a windowShouldClose(_:) message.

If you own the window then isReleasedWhenClosed should be false and the window controller can be removed.

func createWindow() {
    
    let newWindow = NSWindow(contentRect: .init(origin: .zero, size: .init(width: 300, height: 300)),
                             styleMask: NSWindow.StyleMask(rawValue: 0xf),
                             backing: .buffered,
                             defer: false)
    newWindow.isReleasedWhenClosed = false
    newWindow.title = "New Window"
    newWindow.isOpaque = false
    newWindow.isMovableByWindowBackground = true

    newWindow.backgroundColor = NSColor(calibratedHue: 0, saturation: 1.0, brightness: 0, alpha: 0.7)
    newWindow.makeKeyAndOrderFront(nil)

    windows.insert(newWindow)
}
  • Related