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