Search code examples
swiftmacosnswindow

Dynamically create and remove NSWindow


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?


Solution

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