MacOS SwiftUI, how to prevent window close action - swift

So I have a swiftUI application, at some point I create a NSWindow and assign the contentView, like this:
// ////////////////////////////////////////////////////////
// Add token window
// ////////////////////////////////////////////////////////
let configurationView = ConfigurationView().environmentObject(store)
configurationWindow = NSWindow(
contentRect: NSRect(x:0, y: 0, width: 480, height: 500),
styleMask: [.titled, .closable, .miniaturizable, .fullSizeContentView],
backing: .buffered, defer: false
)
configurationWindow.center()
configurationWindow.setFrameAutosaveName("BSchauer")
let hostingController = NSHostingController(rootView: configurationView)
configurationWindow.contentViewController = hostingController
configurationWindow.makeKeyAndOrderFront(nil)
configurationWindow.setIsVisible(false)
...
// later on in the code
#objc func toggleConfigurationWindow() {
if self.configurationWindow.isVisible {
self.configurationWindow.setIsVisible(false)
if let button = self.statusBarItem.button {
self.popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
}
} else {
self.configurationWindow.setIsVisible(true)
self.configurationWindow.contentViewController?.view.window?.becomeKey()
}
}
You see that the way I interact with the window to present it to the user is via the visible flag, now the problem is when the window is shown and closed via the close button on the top bar, the window get somehow unmounted(?) and the next time the user tries to interact with the app and re-open the window I get a segmentation fault.
One of the things I tried was to instead of setting the visibility to false, just re-create the window again, but I still get the segmentation error.
All the previous answers I found are dealing with the old way of dealing with the NSViewController and overriding the windowShouldClose method, but I cannot seem to get that working.
TL:DR: When the user presses the red close button on the window, instead of the window getting destroyed I just want to set its visibility to false

I made it work, no need to set the contentViewController, you can use the standard contentView:
configurationWindow.contentView = NSHostingView(rootView: configurationView)
and to disable the window getting released when closed:
configurationWindow.isReleasedWhenClosed = false
I would still be interested in knowing when the window closed, to maybe perform an action afterwards, but this still solves my problem

Related

MacOS Cocoa, why am i not able to close windows?

i am working in XCode Swift, with Cocoa with Storyboards, and i need to be able to create and close my own windows, programmatically.
to repeat the problem as simply as possible, i start with an empty storyboard project, and change only viewDidLoad in ViewController.swift
override func viewDidLoad() {
super.viewDidLoad()
let rect = NSRect( x:500, y:500, width:800, height:500 )
let mask = NSWindow.StyleMask( arrayLiteral: .closable, .miniaturizable, .resizable, .titled )
let win = NSWindow( contentRect: rect, styleMask: mask, backing: .buffered, defer: false )
win.orderFront( win )
}
the application runs, and puts up two windows. the smaller one is from the storyboard and can be closed without trouble, but when i close the larger window which i created, the application crashes in the appDelegate with
Thread 1: EXC_BAD_ACCESS (code=1, address=0x20)
i assume i am doing something dumb here - what am i missing?
i discovered if i set
win.isReleasedWhenClosed = false
the problem goes away, however i assume the memory is not being released.
i also found that if i append the window handle to an array before closing it, the problem also goes away:
private var broken_windows [NSWindow] = []
public func windowShouldClose( _ sender: NSWindow ) -> Bool {
broken_windows.append( sender )
return true
}
however this array is somehow completely untouchable, and any attempt to access it's members after the window has been closed will cause the application to crash.
In your updated code, the window is only retained because you are holding a strong reference to it via self.win. If you use weak var win: NSWindow?, then self.win will be nil after the window is closed. So, setting isReleasedWhenClosed = false does seem to be a valid solution.

Track NSWindow focus? (Swift, macOS)

I'm trying to track a created windows focus so I can perform actions on those events.
I'm able to track a windows frame dimensions with the notification center using NSView.frameDidChangeNotification, but I cant find a correct way of using something like NSWindowDidResignKeyNotification or NSWindowDidResignMainNotification.
private func newWindow() {
let windowInfo = WindowInfo()
let contentView = ContentView()
.environmentObject(windowInfo)
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 350, height: 600),
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered,
defer: false
)
window.isReleasedWhenClosed = false
window.contentView = NSHostingView(rootView: contentView)
window.contentView?.postsFrameChangedNotifications = true
window.makeKeyAndOrderFront(nil)
// vvv I'm trying to use a similar method of being notified when a created window loses/regains focus. vvv
// v v v
NotificationCenter.default.addObserver(forName: NSView.frameDidChangeNotification, object: nil, queue: nil) { (notification) in
windowInfo.frame = window.frame
}
}
Also, if theres some method for detecting window focus in SwiftUI, I could use that too. The notifications that actions are performed on are for my Views in SwiftUI
This was just a dumb oversight from me (it was pretty late at night), and I found the line I needed.
Adding NSWindow.didBecomeKeyNotification did the trick to properly catch the focus events.
Also changing object: nil to object: window made sure the focus events caught were only for the specific window, instead of all.

NSPanel not hiding when focus is lost

I am trying to create a window like Spotlight.
As in Spotlight it should hide when the background is clicked. I tried doing it unsuccessfully with NSWindow but I was lead to believe using NSPanel instead would solve the problem.
However, even when using NSPanel the window does not hide.
Here is the code I'm using.
let panel = NSPanel(contentRect: CGRect(x: 0, y: 0, width: 200, height: 200), styleMask: [.titled, .nonactivatingPanel], backing: .buffered, defer: true)
panel.level = .mainMenu
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
panel.orderFrontRegardless()
It is due to used window level (.mainMenu which is above all windows), so you need to hide it explicitly via delegate methods
so assuming you create window/panel in your controller, make that controller a delegate of window
panel.delegate = self
and implement something like
extension ViewController { // << your controller class here
func windowDidResignKey(_ notification: Notification) {
if let panel = notification.object as? NSWindow {
panel.close()
}
}
}

Swift - how to make window unactivable?

I want to make application unactivable.
I mean its must be inactive and non-foreground even by click on it. Active window must be the same app as it was before click on my app.
How can I do this?
upd:
Let's imagine some window/panel. Like a Dock. Let's call it "Docky"
Docky is inactive, Active window is Safari:
NSRunningApplicationSafari.isActive == true
NSRunningApplicationDocky.isActive == false
I'm clicking on ANY empty point of Docky.
Expected result:
Safari is still active window. Like before click:
NSRunningApplicationSafari.isActive == true
NSRunningApplicationDocky.isActive == false
Actual result:
Docky is active window, Safari is inactive:
NSRunningApplicationSafari.isActive == false
NSRunningApplicationDocky.isActive == true
Another sample is Keyboard Viewer. You are clicking on the virtual keyboard, but active window is another app. Ant exactly active window getting keyPress events.
Actually all you need is .nonactivatingPanel style panel. Everything else is details, like level of this window, custom views with overridden acceptsFirstMouse:, needsPanelToBecomeKey, etc. Btw, button accepts first click by default, non activating app in this case.
So your AppDelegate, for example, might look like the following:
class AppDelegate: NSObject, NSApplicationDelegate {
var docky: NSPanel!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the window and set the content view.
docky = NSPanel(
contentRect: NSRect(x: 0, y: 0, width: 120, height: 600),
styleMask: [.nonactivatingPanel],
backing: .buffered, defer: false)
docky.level = .mainMenu
....
docky.orderFrontRegardless()
}

CGDisplayCreateImage only returns wallpaper and task bar

I am trying to get the pixel colors from the screen for a specified portion of the display.
I am using CGDIsplayCreateImage(_:) to do so, but for some reason instead of creating a screenshot of the currently open windows it just gives an image of the wallpaper and the task bar.
In order to visualize the screenshot I am using a SwiftUI view. In my application I actually don't need the screenshot displayed.
struct ScreenshotView: View {
let cgImage: CGImage
var body: some View {
Image(decorative: cgImage, scale: 3)
}
}
And the applicationDidFinishLaunching(_:) methods looks like this.
func applicationDidFinishLaunching(_ aNotification: Notification) {
let contentRect = NSRect(x: 0, y: 0, width: 240, height: 240)
// Create the window and set the content view.
window = NSWindow(
contentRect: contentRect,
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
window.center()
window.setFrameAutosaveName("Main Window")
window.contentView = NSHostingView(rootView: ScreenshotView(cgImage: CGDisplayCreateImage(CGMainDisplayID())!))
window.makeKeyAndOrderFront(nil)
}
Since XCode and some other apps are open it should give me a screenshot of the current screen with all open windwos. Instead I get a screenshot of the wallpaper and the taskbar showing my apps name. No apps are showing and the dock is also missing.
Am I doing something wrong or is this a bug?
In this stackoverflow answers suggest that I am using the function correctly.
Even if this might be obvious to some people I will answer my own question here using the help of Ken Thomases.
Below you can find the settings I set within my target. It now works after every rebuild.
Make sure to deactivate automatic signing and set the signing certificate to 'Sign to Run Local'.
Hope this helps someone at some point.