I have code that opens a window and displays a view on a connected display. My goal is to detect a connection/disconnection of a connected display and show/remove the view accordingly. I have that part working fine.
The problem I am having is closing the window upon disconnection, but then if a subsequent connection is made, and upon creating the window and view again, I get a EXC_BAD_ACCESS
error.
I tried a different approach by setting the connectedDisplayWindow
and connectedDisplayView
to nil
, after calling close()
on the window when a connected display is removed. Maybe I am misunderstanding the close()
method?
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...
Just to make sure, I tried setting the isReleasedWhenClosed
to true, but it did not change the problem.
The other thing I see in the console is about seven repeated error strings immediately upon disconnection of the connected display: 2022-04-10 10:28:11.044155-0500 External Display[95744:4934855] [default] invalid display identifier 67EE0C44-4E3D-3AF2-3447-A867F9FC477D
before the notification is fired, and one more after the notification occurs: 2022-04-10 10:28:11.067555-0500 External Display[95744:4934855] [default] Invalid display 0x4e801884
. Could these be related to the issues I am having?
Full example code:
ViewController.swift
import Cocoa
let observatory = NotificationCenter.default
class ViewController: NSViewController {
var connectedDisplay: NSScreen?
var connectedDisplayWindow: NSWindow?
var connectedDisplayView: NSView?
var connectedDisplayCount: Int = 0
var connectedDisplayID: UInt32 = 0
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
setupObservatory()
if NSScreen.screens.count > 1 {
handleDisplayConnectionChange(notification: nil)
}
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
override func viewWillDisappear() {
connectedDisplayWindow?.close()
}
func setupObservatory() {
observatory.addObserver(self, selector: #selector(handleDisplayConnectionChange), name: NSApplication.didChangeScreenParametersNotification, object: nil)
observatory.addObserver(forName: .setupConnectedDisplayWindow, object: nil, queue: nil, using: setupConnectedDisplayWindow)
}
@objc func handleDisplayConnectionChange(notification: Notification?) {
if connectedDisplayCount != NSScreen.screens.count {
if connectedDisplayCount < NSScreen.screens.count {
print("There is a connected display.")
connectedDisplayCount = NSScreen.screens.count
if let _ = NSScreen.screens.last {
if connectedDisplay != NSScreen.screens.last {
connectedDisplayID = NSScreen.screens.last!.displayID!
connectedDisplay = NSScreen.screens.last!
}
} else {
connectedDisplayID = 0
}
if connectedDisplayID != 0 && !connectedDisplayIsActive {
observatory.post(name: .setupConnectedDisplayWindow, object: nil)
}
} else if connectedDisplayCount > NSScreen.screens.count {
print("A connected display was removed.")
connectedDisplayCount = NSScreen.screens.count
connectedDisplayIsActive = false
connectedDisplayWindow?.close()
//connectedDisplayView = nil <- causes error @main in AppDelegate
//connectedDisplayWindow = nil <- causes error @main in AppDelegate
connectedDisplay = nil
connectedDisplayID = 0
}
}
}
func setupConnectedDisplayWindow(notification: Notification) {
if NSScreen.screens.count > 1 && !connectedDisplayIsActive {
connectedDisplay = NSScreen.screens.last
let mask: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable]
connectedDisplayWindow = NSWindow(contentRect: connectedDisplay!.frame, styleMask: mask, backing: .buffered, defer: true, screen: connectedDisplay) // <- causes error on subsequent connection
connectedDisplayWindow?.level = .normal
connectedDisplayWindow?.isOpaque = false
connectedDisplayWindow?.backgroundColor = .clear
connectedDisplayWindow?.hidesOnDeactivate = false
let viewRect = NSRect(x: 0, y: 0, width: connectedDisplay!.frame.width, height: connectedDisplay!.frame.height)
connectedDisplayView = ConnectedDisplayView(frame: viewRect)
connectedDisplayWindow?.contentView = connectedDisplayView
connectedDisplayWindow?.orderFront(nil)
connectedDisplayView?.window?.toggleFullScreen(self)
connectedDisplayIsActive = true
observatory.post(name: .setupConnectedDisplayView, object: nil)
}
}
}
extension Notification.Name {
static var setupConnectedDisplayWindow: Notification.Name {
return .init(rawValue: "ViewController.setupConnectedDisplayView")
}
static var setupConnectedDisplayView: Notification.Name {
return .init(rawValue: "ConnectedDisplayView.setupConnectedDisplayView")
}
}
extension NSScreen {
var displayID: CGDirectDisplayID? {
return deviceDescription[NSDeviceDescriptionKey(rawValue: "NSScreenNumber")] as? CGDirectDisplayID
}
}
ConnectedDisplayView.swift
import Cocoa
var connectedDisplayIsActive: Bool = false
class ConnectedDisplayView: NSView {
var imageView: NSImageView!
override init(frame: NSRect) {
super.init(frame: frame)
setupObservatory()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupObservatory() {
observatory.addObserver(forName: .setupConnectedDisplayView, object: nil, queue: nil, using: setupConnectedDisplayView)
}
func setupConnectedDisplayView(notification: Notification) {
let imageURL = URL(fileURLWithPath: "/Users/Shared/my image.png")
if let image = NSImage(contentsOf: imageURL) {
imageView = NSImageView(image: image)
imageView.wantsLayer = true
imageView.frame = self.frame
imageView.alphaValue = 1
self.addSubview(imageView)
}
}
}
I commented out the nil settings for the connectedDisplayWindow
and connectedDisplayView
objects and the error at @main
in AppDelegate went away, but then I get an error when trying to reinitialize the connectedDisplayWindow
if the connected display is removed or the connection is momentarily interrupted.
CodePudding user response:
The default value of isReleasedWhenClosed
is true
and connectedDisplayWindow?.close()
releases the window. Setting connectedDisplayWindow
to nil
or to another window releases the window again and causes a crash. Solution: set isReleasedWhenClosed
to false
.