window.windowController is nil inside windowWillClose() but it isn't inside viewDidAppear() - swift

I've tried, without success, respond to events such as windowWillClose() and windowShouldClose() inside NSWindowController (yes conforming to NSWindowDelegate).
Later, to my surprise, I was able to receive those events if I make my contentViewController (NSViewController) conform to NSWindowDelegate.
Unfortunately, later on, found out that view.window?.windowController is nil inside windowWillClose() or windowShouldClose(), code:
override func viewDidAppear() {
super.viewDidAppear()
self.view.window?.delegate = self
self.view.window?.windowController // not nil!
}
func windowWillClose(_ notification: Notification) {
self.view.window?.windowController // nil!!
}
func windowShouldClose(_ sender: NSWindow) -> Bool {
self.view.window?.windowController // nil!!
return true
}
After realizing that view.window?.windowController is not nil inside viewDidAppear() the next thing I thought was that Swift garbage collected the controller, so I changed viewDidAppear() in a way that creates another reference of windowController thus preventing garbage collection on said object, code:
var windowController: NSWindowController?
override func viewDidAppear() {
super.viewDidAppear()
self.view.window?.delegate = self
windowController = view.window?.windowController
}
func windowWillClose(_ notification: Notification) {
self.view.window?.windowController // NOT nil
}
func windowShouldClose(_ sender: NSWindow) -> Bool {
self.view.window?.windowController // NOT nil
return true
}
My hypothesis turned out to be correct (I think).
Is this the same issue that is preventing me from receiving those events inside NSWindowController?
Is there another way I can achieve the same thing without creating more object references?

In order to post code, I use the Answer option even though it is more of a comment.
I added in NSViewController:
override func viewDidAppear() {
super.viewDidAppear()
parentWindowController = self.view.window!.windowController
self.view.window!.delegate = self.view.window!.windowController as! S1W2WC. // The NSWC class, which conforms to NSWindowDelegate
print(#function, "windowController", self.view.window!, self.view.window!.windowController)
}
I get print log:
viewDidAppear() windowController Optional()
and notification is passed.
But if I change to
override func viewDidAppear() {
super.viewDidAppear()
// parentWindowController = self.view.window!.windowController
self.view.window!.delegate = self.view.window!.windowController as! S1W2WC
print(#function, "windowController", self.view.window!, self.view.window!.windowController)
}
by commenting out parentWindowController, notification don't go anymore to the WindowController…
Edited: I declared in ViewController:
var parentWindowController: NSWindowController? // Helps keep a reference to the controller

The proposed solutions are, in my opinion, hacks that can cause serious problems with memory management by creating circular references. You definitely can make instances of NSWindowController work as the window’s delegate. The proper way is to wire it up correctly in either code or in Interface Builder in Xcode. An example of how to do it properly is offered here.
If the delegate methods are not called is because the wiring up is not done correctly.
Another thing that must be done in Swift is when you add the name of the NSWindowController subclass in Interface Builder in Xcode is to check the checkbox of Inherits from Module. If you fail to do this, none of your subclass methods will be called.

Related

NSWindowDelegate not getting resize notifications

In a macOS Cocoa app, I am trying to get notifications when my window is "corner dragged" to a new size by setting an NSWindow delegate. I am successfully getting notified of resize events when the window is initially created, but not when the window is later dragged to a new size. I can't figure out why not.
Here is my code:
class MyWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
window?.delegate = self
}
}
extension MyWindowController: NSWindowDelegate {
func windowDidResize(_ notification: Notification) {
print("windowDidResize")
}
func windowWillResize(_ sender: NSWindow, to frameSize: NSSize) -> NSSize {
print("windowWillResize")
return frameSize
}
}
When the window is first created, I see this output:
windowWillResize
windowDidResize
which proves the delegate methods are working. However, when I then grab a corner of the window and resize it, I do not see any additional print output.
I have reviewed a number of similar questions on SO about getting these notifications (like this and this), and it seems like I am doing everything right. And yet I don't get the notifications on window corner drag resize. Does anyone know why?
Based on the comment from Willeke, I created a strong reference to the NSWindowController subclass in my AppDelegate (where the window is created) and it fixed the problem.
For people finding this in the future, this is how I created in the strong reference (in AppDelegate):
// Need to maintain a strong reference while window is open
var myWindowController : MyWindowController?
[...]
func funcThatCreatesWindow() {
[...]
self.myWindowController = storyboard.instantiateController(withIdentifier: storyboardID) as? MyWindowController
if self.myWindowController != nil {
myWindowController!.showWindow(nil)
}
}

Prevent NSToolbarItem from being removed

I want to prevent certain toolbar items from being removed by the user. They should still be movable, just not removable.
I tried creating a custom subclass of NSToolbar with a custom removeItem(at:) implementation, but it seems this method is not even called if the user drags an item out of the toolbar in the customization palette.
The delegate also doesn't seem to expose functionality for this.
How can I disable removal of certain NSToolbarItems?
I am not sure if you can prevent it from being removed but you can implement the optional toolbarDidRemoveItem method and insert the item that you don't want it to be removed back:
import Cocoa
class WindowController: NSWindowController, NSToolbarDelegate {
#IBOutlet weak var toolbar: Toolbar!
override func windowDidLoad() {
super.windowDidLoad()
toolbar.delegate = self
}
func toolbarDidRemoveItem(_ notification: Notification) {
if let itemIdentifier = (notification.userInfo?["item"] as? NSToolbarItem)?.itemIdentifier,
itemIdentifier.rawValue == "NSToolbarShowColorsItem" {
toolbar.insertItem(withItemIdentifier: itemIdentifier, at: 0)
}
}
}
Since it is not super critical if they are removed in case a private API call would stop working, I opted for the private API solution.
extension NSToolbarItem {
func setIsUserRemovable(_ flag: Bool) {
let selector = Selector(("_setIsUserRemovable:"))
if responds(to: selector) {
perform(selector, with: flag)
}
}
}
This works exactly as advertised.

ViewController + Storyboard setting up validation with controlTextDidChange

Trying to setup validation for a few text fields in a new (and very small) Swift Mac app. Following various other topics here on SO and a few other examples, I can still not get controlTextDidChange to propagate (to my ViewController).
E.g: How to live check a NSTextField - Swift OS X
I have read at least a dozen variations of basically that same concept. Since none of the accepted answers seem to work I am just getting more and more confused by something which is generally a fairly simple task on most platforms.
I have controlTextDidChange implemented to just call NSLog to let me know if I get anything.
AppDelegate should be part of the responder chain and should eventually handle controlTextDidChange but I see nothing there either.
Using the current Xcode I start a new project. Cocoa app, Swift, Storyboard and nothing else.
From what I can gather the below isolated example should work. In my actual app I have tried some ways of inserting the ViewController into the responder chain. Some answers I found suggested it was not always there. I also tried manually adding the ViewController as the delegate in code theTextField.delegate = self
Nothing I have done seems to get text changed to trigger any events.
Any ideas why I have so much trouble setting up this delegation?
My single textfield example app
Storyboard is about as simple as it gets:
AppDelegate
import Cocoa
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSTextFieldDelegate, NSTextDelegate {
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Insert code here to initialize your application
}
func applicationWillTerminate(_ aNotification: Notification) {
// Insert code here to tear down your application
}
func controlTextDidChange(notification: NSNotification) {
let object = notification.object as! NSTextField
NSLog("AppDelegate::controlTextDidChange")
NSLog("field contains: \(object.stringValue)")
}
}
ViewController
import Cocoa
class ViewController: NSViewController, NSTextFieldDelegate, NSTextDelegate {
#IBOutlet var theTextField: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
func controlTextDidChange(notification: NSNotification) {
let object = notification.object as! NSTextField
NSLog("ViewController::controlTextDidChange")
NSLog("field contains: \(object.stringValue)")
}
}
I think the samples you're following are a bit out-of-date.
Try...
override func controlTextDidChange(_ notification: Notification) {
...as the function definition for your method in your NSTextFieldDelegate.

windowShouldClose reporting unresolved identifier

I'm trying to trigger a save of my CoreData on a window close as its only a single window application.
I've got the following code in my viewDidLoad and viewDidAppear
override func viewDidLoad() {
super.viewDidLoad()
if windowShouldClose(self) {
saveValues()
}
}
override func viewDidAppear() {
super.viewDidAppear()
self.view.window?.delegate = self
}
however im still getting the following error
Use of unresolved identifier 'windowShouldClose'
Any advice as to why i'm still getting this error after declaring the window delegate as self?
Set the delegate in viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
self.view.window?.delegate = self
}
and implement the delegate method
func windowWillClose(notification: NSNotification) {
saveValues()
}
windowShouldClose is different. It asks for permission to close the window and expects a boolean return value.

Swift TextField Method Not Called, Delegate is Set, now BAD_ACCESS Errors

there are many similar questions about TextFields delegate method textfieldshouldreturn not being called, but all were solved by setting the delegate. Ive set the delegate, and also have a perfectly fine example in another project I've copied almost line for line. A print statement confirms no call is made. Whats more curious is that I set a random variable to test if I was even accessing the right object, but when I tried to access that variable, it crashed with a BAD_ACCESS error.
class TitleTextField: UITextField, UITextFieldDelegate {
var randomElement: Bool = true
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
print("text field return pressed")
return true
}
}
and here is where I'm using it
class EditViewController: UIViewController {
#IBOutlet weak var titleTextField: TitleTextField!
func configureView() {
navigationItem.title = "Edit Goal"
}
override func viewDidLoad() {
super.viewDidLoad()
print("editor loaded")
configureView()
titleTextField.text = "placeholder"
titleTextField.delegate = titleTextField
titleTextField.delegate = titleTextField.self
if let textField = titleTextField {
textField.delegate = titleTextField
}
print("textfield delegate = \(titleTextField?.delegate)")
}
If listed some of the different ways I tried setting the delegate. I even conformed the viewController to UITextFieldDelegate and set the delegate to self but that didn't matter either. I added "randomVariable" to TitleTextField to make sure I was accessing the correct object, but when I used titleTextField.randomVariable = true in viewDidLoad, I got a BAD_ACCESS crash.
Ive also double checked the storyboard connection. I even deleted the connection and IBoutlet and redid them, no difference. cleaned project etc.
Wow ok, so the problem was I hadnt set the textfield class to TitleTextField in my identity inspector. I had it programmatically set, I guess I didnt realize i had to do it in the storyboard too.
The issue is that you're conforming to the UITextFieldDelegate on your custom TitleTextField itself. Instead, you should conform to the protocol on your UIViewController, like so:
class EditViewController: UIViewController, UITextFieldDelegate {
#IBOutlet weak var titleTextField: TitleTextField!
func configureView() {
navigationItem.title = "Edit Goal"
}
override func viewDidLoad() {
super.viewDidLoad()
print("editor loaded")
configureView()
titleTextField.text = "placeholder"
titleTextField.delegate = self
print("textfield delegate = \(titleTextField?.delegate)")
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
print("text field return pressed")
return true
}
The purpose of the delegate is to respond to editing-related messages from the text field (link to docs). This means that the UITextField is already aware of these editing events. What you need to do is allow the class containing your custom UITextField to listen to the events that it is sending out. In your situation, that class is EditViewController. You can make EditViewController listen to the UITextView's events by setting it as the delegate.
The reason for your BAD_ACCESS error is a memory-related issue. Your UITextField is calling itself infinitely through recursion. If you look through the calling stack you'll probably see it calling the same method hundreds of times. See this post for more insight.