Dynamically create and remove NSWindow - swift

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?

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

Related

How can I set the main Window for my App in macOS Cocoa?

I am making intentionally 2 Window in my AppDelegate, when I lunch my app both of those get lunched as well, but I want have control on which one should be lunched as main window, how can I create and have multi window in AppDelegate and also having control which window is going to be main, I do not want having 2 window opened in same time in lunch. Also I do not want to lunch both and hide another one, my goal is having those created window, but also having control to lunch the wished one as main and maybe later using the second one on wish.
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {
private let window: NSWindow = NSWindow()
private let window2: NSWindow = NSWindow()
func applicationDidFinishLaunching(_ aNotification: Notification) {
window.setFrame(CGRect(origin: .zero, size: CGSize(width: 500, height: 500)), display: true)
window.styleMask = [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView]
window.backingType = .buffered
window.title = "window"
window.center()
window.makeKeyAndOrderFront(window)
window2.setFrame(CGRect(origin: .zero, size: CGSize(width: 500, height: 500)), display: true)
window2.styleMask = [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView]
window2.backingType = .buffered
window2.title = "window2"
window2.center()
window2.makeKeyAndOrderFront(window2)
}
}
Also I am using a main file like this:
import Cocoa
let nsApplication: NSApplication = NSApplication.shared
let appDelegate: AppDelegate = AppDelegate()
nsApplication.delegate = appDelegate
nsApplication.run()

Opening an NSWIndow / NSView on a connected display

Environment: macOS Big Sur 11.5.2 / Swift 5.4
Target Platform: macOS
I came across this discussion while searching for how to open a new NSWindow / NSView on a connected display.
I converted the obj-c code to Swift, but I cannot get it to work. I have the method set up inside an IBAction and I linked the action to a button I placed in the view titled "Show External Display."
What am I doing wrong?
import Cocoa
class ViewController: NSViewController {
var externalDisplay: NSScreen?
var fullScreenWindow: NSWindow?
var fullScreenView: NSView?
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
#IBAction func showExternalDisplayButtonClicked(sender: Any?) {
// Open a window on the external display if present
if NSScreen.screens.count > 1 {
externalDisplay = NSScreen.screens.last
let externalDisplayRect = externalDisplay!.frame
fullScreenWindow = NSWindow(contentRect: externalDisplayRect, styleMask: .borderless, backing: .buffered, defer: true, screen: externalDisplay)
fullScreenWindow!.level = .normal
fullScreenWindow!.isOpaque = false
fullScreenWindow!.hidesOnDeactivate = false
fullScreenWindow!.backgroundColor = .red
let viewRect = NSRect(x: 0, y: 0, width: externalDisplay!.frame.width, height: externalDisplay!.frame.height)
fullScreenView = NSView(frame: viewRect)
fullScreenWindow!.contentView = fullScreenView
fullScreenWindow!.makeKeyAndOrderFront(self)
}
}
}
Why do you make styleMask: .borderless? Docs say a borderless window can't become key or main so makeKeyAndOrderFront fails.
If you make a normal window like this:
let mask: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable]
fullScreenWindow = NSWindow(contentRect: externalDisplayRect, styleMask: mask, backing: .buffered, defer: true, screen: externalDisplay)
it opens on the last screen.

How to convert a Swift UI View to a NSImage

I am trying to use a SwiftUI View as a NSCursor on MacOS.
Using SwiftUI I am constructing a view that I then convert to an NSView using NSHostingView. Now I am trying to convert that to a NSImage via a NSBitmapImageRep. For some reason I always get an empty png when I inspect the variables while setting breakpoints.
(For now the setup of the cursor is done in the AppDelegate because I am currently just trying to get this to work.)
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
let contentRect = NSRect(x: 0, y: 0, width: 480, height: 480)
// 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: contentView)
window.makeKeyAndOrderFront(nil)
let myNSView = NSHostingView(rootView: ContentView()).bitmapImageRepForCachingDisplay(in: contentRect)!
NSHostingView(rootView: ContentView()).cacheDisplay(in: contentRect, to: myNSView)
let myNSImage = NSImage(size: myNSView.size)
myNSImage.addRepresentation(myNSView)
self.window.disableCursorRects()
// var myName = NSImage.Name("ColorRing")
// var myCursorImg = NSImage(named: myName)!
//
// var myCursor = NSCursor(image: myCursorImg, hotSpot: NSPoint(x: 10, y: 10))
var myCursor = NSCursor(image: myNSImage, hotSpot: NSPoint(x: 0, y: 0))
myCursor.set()
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
}
When executing the part that is commented out, I get the mouse cursor from an external png file.
When executing the code shown above I always get an empty mouse cursor with the size 480x480.
In debug mode I can see, that myNSView is an empty png.
I am pretty sure I misunderstand the documentation and therefore misuse cacheDisplay(in:to:) and bitmapImageRepForCachingDisplay(in:).
The documentation tells me to use the NSBitmapImageRep from bitmapImageRepForCachingDisplay(in:) in cacheDisplay(in:to:). But for some reason I simply can not get this to work.
Does anyone have an idea what I am doing wrong?
To answer my own question:
It seems like the view that one wants to convert to an NSImage needs to be layed out inside of a window. Otherwise it probably does not know how large the view needs to be inside the rect.
The following code works for me:
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the SwiftUI view that provides the window contents.
let contentView = ContentView()
let contentRect = NSRect(x: 0, y: 0, width: 480, height: 480)
// 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: contentView)
window.makeKeyAndOrderFront(nil)
// CHANGED ----------
let newWindow = NSWindow(
contentRect: contentRect,
styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
backing: .buffered, defer: false)
newWindow.contentView = NSHostingView(rootView: contentView)
let myNSBitMapRep = newWindow.contentView!.bitmapImageRepForCachingDisplay(in: contentRect)!
newWindow.contentView!.cacheDisplay(in: contentRect, to: myNSBitMapRep)
// CHANGED ----------
let myNSImage = NSImage(size: myNSBitMapRep.size)
myNSImage.addRepresentation(myNSBitMapRep)
self.window.disableCursorRects()
var myCursor = NSCursor(image: myNSImage, hotSpot: NSPoint(x: 0, y: 0))
myCursor.set()
}
Only lines between the two //CHANGED ----- comments have been changed for a working solution. I am simply embedding the view inside a window before applying the same methods as before.
Hope this helps some people in the future.

NSWindow Location

I am programmatically creating a window that needs to span across 2 screens. the size of the window being created is correct but the window is starting about halfway across the first screen. I can drag it back to the beginning of the first screen and the NSWindow fits perfectly.
I just need to know where I'm going wrong in terms of the starting point for the window.
func createNewWindow() {
let bannerWidth = NSScreen.screens[0].frame.width * 2
let bannerHeight = NSScreen.screens[0].frame.height
let rect = CGRect(origin: CGPoint(x: 0,y :0), size: CGSize(width: bannerWidth, height: bannerHeight))
let newWindow = NSWindow(contentRect: rect, styleMask: [.closable], backing: .buffered, defer: false)
let storyboard = NSStoryboard(name: "Main", bundle: nil).instantiateController(withIdentifier: "ExternalScreen") as? NSViewController
let window = storyboard?.view
newWindow.styleMask.update(with: .resizable)
NSMenu.setMenuBarVisible(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.toggleFullScreen(true)
newWindow.makeKeyAndOrderFront(nil)
newWindow.toolbar?.isVisible = false
newWindow.contentView?.addSubview(window!)
}
You're setting the content rectangle, but not setting the frame. The frame is the rectangle that the window occupies in screen coordinates. To move the window, you can setFrameOrigin() or setFrameTopLeftPoint().

CALayer can't be seen

I'm trying to learn swift. So far, my code runs, but it doesn't show a CAlayer I create:
class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
var mainview: NSView!
var redLayer: CATextLayer!
func applicationDidFinishLaunching(aNotification: NSNotification) {
self.mainview = NSView(frame: NSRect(x:0,y:0,width:300,height:300))
self.window = NSWindow(contentRect: NSMakeRect(0, 0, 300, 300),
styleMask: NSTitledWindowMask | NSClosableWindowMask |
NSMiniaturizableWindowMask,
backing: .Buffered, `defer`: false)
self.window.contentView!.addSubview(self.mainview)
self.window.makeKeyAndOrderFront(self.window)
self.window.makeMainWindow()
self.redLayer = CATextLayer()
self.redLayer.string = "test"
self.redLayer.frame = self.mainview.bounds
self.redLayer.bounds = CGRect(x:50,y:50,width:300,height:300)
self.redLayer.position = CGPoint(x: 50, y: 50)
self.redLayer.backgroundColor = CGColorCreateGenericRGB(1.0, 0.0, 0.0, 1.0)
self.redLayer.foregroundColor = CGColorCreateGenericRGB(0.0, 0.0, 0.0, 1.0)
self.redLayer.hidden = false
self.mainview.layer = self.redLayer
}
func applicationWillTerminate(aNotification: NSNotification) {
// Insert code here to tear down your application
}}
the code gives a gray empty window. but I want it to show a colored window with a line of text saying "test", but I can't see these stuff.
I tried multiple ways, I can't fix this issue.
In OS X, Core Animation support (CALayer) is not enabled automatically. You have to manually enable it. For a programmatically created view, use the wantsLayer property.
self.mainview.wantsLayer = true