如何正确处理 Connected Display Window 和 View?

How to properly dispose of Connected Display Window and View?

我有代码可以打开 window 并在连接的显示器上显示视图。我的目标是检测连接显示器的 connection/disconnection 和相应的视图 show/remove。我的那部分工作正常。

我遇到的问题是在断开连接时关闭 window,但是如果建立后续连接,并且在创建 window 并再次查看时,我会得到一个 EXC_BAD_ACCESS 错误。

我尝试了一种不同的方法,在连接的显示器是删除。也许我误解了 close() 方法?

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...

为了确定,我尝试将 isReleasedWhenClosed 设置为 true,但并没有改变问题。

我在控制台中看到的另一件事是在连接的显示器断开连接后立即重复出现 7 个错误字符串:2022-04-10 10:28:11.044155-0500 External Display[95744:4934855] [default] invalid display identifier 67EE0C44-4E3D-3AF2-3447-A867F9FC477D 在触发通知之前,在通知发生之后还有一个:2022-04-10 10:28:11.067555-0500 External Display[95744:4934855] [default] Invalid display 0x4e801884.这些可能与我遇到的问题有关吗?

完整示例代码:

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

我注释掉了 connectedDisplayWindowconnectedDisplayView 对象的 nil 设置,AppDelegate 中 @main 处的错误消失了,但是当我尝试重新初始化时出现错误connectedDisplayWindow 如果连接的显示器被移除或连接暂时中断。

isReleasedWhenClosed的默认值为trueconnectedDisplayWindow?.close()释放window。将 connectedDisplayWindow 设置为 nil 或另一个 window 会再次释放 window 并导致崩溃。解决方法:将isReleasedWhenClosed设置为false.