Home > front end >  How to properly dispose of Connected Display Window and View?
How to properly dispose of Connected Display Window and View?

Time:04-11

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?

Apple Documentation

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.

  • Related