New window opens incorrectly - macos

I want to load a new window after clicking a button. This code opens new window for about 0.01 ms and close. What I'm doing wrong?
#IBAction func goToSettings(sender: AnyObject) {
let s = SettingsViewController(windowNibName: "SettingsViewController")
s.showWindow(sender)
}
The button is located in popover at menu bar.

The controller is stored in a local variable. After your goToSettings() method exits, there's no strong reference to it anymore. So, it's released and it releases the window that it owns.
You need to store a strong reference to it in some longer-lived variable, such as an instance variable of whatever class has that goToSettings() method.

Related

How to exclude certain AppKit views from restorable NSWindow?

NSWindows can be made restorable so that their configuration is preserved between application launches.
https://developer.apple.com/documentation/appkit/nswindow/1526255-restorable
Windows should be preserved between launch cycles to maintain interface continuity for the user. During subsequent launch cycles, the system tries to recreate the window and restore its configuration to the preserved state. Configuration data is updated as needed and saved automatically by the system.
In a new macOS project, the NSWindow on a Storyboard is restorable by default:
My problem comes when embedding an NSTabViewController in the NSWindow.
The NSTabView is inheriting the window's restorable state automatically, with no added code.
This makes the selected tab persist between app launches. I don't want that. I want it to always default to index 0. If the selected tab is restored, attempting to select a tab programmatically in viewDidLoad has unexpected results.
How can I force certain AppKit UI elements to be excluded from NSWindow state restoration?
I want the Tab View to be un-restorable.
But I would like to keep other restorable benefits, such as restoring the previously-set window size.
How can single views be excluded from NSWindow state restoration?
AFAIK, you cannot exclude a certain part of the UI from being restorable. It is an either ON or OFF thing for all elements. That's why I rarely use Apple's own restorability APIs, as more often than not, they are unreliable. I always do the restoration myself to get that fine control that you need. For simpler windows, however, I let the system do the restoration.
After this preamble, and to really answer your question, I rarely use viewDidLoad() to set up any windows, because as you found out that has some nasty consequences (e.g., the window might not exist yet!). I always do that in viewWillAppear(). For that to happen, you need to set up the following:
You need to have an ivar (let's call it tabViewController) to your NSTabViewController instance in your parent NSViewController (let's call it NSViewMainController)
Override prepare(for segue: NSStoryboardSegue, sender: Any?) in NSViewMainController and set up the NSTabViewController and its NSViewController children like this:
override func prepare(for segue: NSStoryboardSegue, sender: Any?) {
// set up the tabViewController ivar
self.tabViewController = segue.destinationController as? NSTabViewController
// set up the child NSViewControllers if you need to access them via their parent (otherwise this step is not needed)
if let childControllers = tabViewController?.children {
for controller in childControllers {
if let controller = controller as? NSViewController1 {
childController1 = controller
}
else if let controller = controller as? NSViewController2 {
childController2 = controller
}
else if let controller = controller as? NSViewController3 {
childController3 = controller
}
}
}
}
Override viewWillAppear() of NSViewMainController and then set up the desired tabView:
guard let controller = tabViewController else { return }
controller.selectedTabViewItemIndex = 0
Major caveat: Beware of viewWillAppear(), though... Unlike viewDidLoad(), this override can be called multiple times, and thus you need to take that into account in your code and react appropriately.
The key to state restoration is the NSResponder method restoreStateWithCoder:
This method is part of the window restoration system and is called at launch time to restore the visual state of your responder object. The default implementation does nothing but specific subclasses (such as NSView and NSWindow) override it and save important state information. Therefore, if you override this method, you should always call super at some point in your implementation.
https://developer.apple.com/documentation/appkit/nsresponder/1526253-restorestate
So, to not restore a certain control, make this method a no-op.
It says that you "should always call super", but that restores the window state. So if you don't want window restoration, don't call super.
In the case of a Tab View, it evidently must be done in the NSTabView (subclass) itself. In other views, overriding this method on the View Controller may work.
class SomeTabView: NSTabView {
override func restoreState(with coder: NSCoder) {
// Do NOT restore state
}
}

Show sheet when the main window first loads

I am trying to get a sheet to load when the main window first loads.
This sheet is so that it asks the user for a file so that they are sort of forced to open a file for use in the program when they first start.
I tried to put performSegue(withIdentifier: sender:) in viewDidLoad(), however it just loads the sheet and nothing else.
override func viewDidLoad() {
super.viewDidLoad()
performSegue(withIdentifier: NSStoryboardSegue.Identifier(rawValue: "browse"), sender: self)
}
I want the main window to load, then the sheet to slide down immediately after asking the user to open a file.
Where should I put the performSegue or is there another way I should be doing this?
#mschmidt is right, I believe; you'll need to perform the segue from viewDidAppear() or windowDidBecomeMain() or similar; the view needs to have been displayed before you can segue from it.
Note that viewDidAppear() will fire when the view is drawn, whether or not it has focus; windowDidBecomeMain() will only fire when your app becomes the frontmost app; you may be working in another app but still be able to see your app's windows on-screen, and in this state windowDidBecomeMain() may not have fired yet.
And yes, both viewDidAppear() and windowDidBecomeMain() will fire every time the view appears or the window becomes main, meaning if you minimise the app then un-minimise it, both will fire again. So you'll need some way of ensuring your segue doesn't show again each time, unless you want it to. A simple boolean flag eg segueHasShown should achieve that.

Closing window in swift

I have a collection view and each item opens a new window with a slideshow.
The segue kind is "show" and it's opened with:
performSegue(withIdentifier: "showGalleryPlayer", sender: self)
My idea is when double click another item the window with the previous slideshow close and the new one opens. I don't know how to do it or if it is the right approach. I want only one window with the slideshow at a time.
Thanks.
When performing a segue, the following method will be invoked:
func prepare(for segue: NSStoryboardSegue, sender: Any?)
Just override this method in whichever class you perform the segue and use the provided NSStoryboardSegue object to retrieve a reference to the newly opened window (after matching the segue's identifier of course). Store the reference to the window and use it to close the window before opening the next one.

How to enable undo menu item when a sheet is presented

I am creating a document based core data OSX app using storyboards. Undo and redo works fine until I present a View Controller in a sheet with a segue. Once the sheet is presented, the undo / redo buttons are grayed out.
While searching for a possible solution, I came across this article in which they say that I have to supply an undo manager to my window using the
"windowWillReturnUndoManager:" delegate method. So I implemented this method in the sourceController of my segue, and set that controller as the delegate for the window of the destinationController in the prepareForSegue method like this:
override func prepareForSegue(segue: NSStoryboardSegue, sender: AnyObject?) {
super.prepareForSegue(segue, sender: sender)
(segue.destinationController as NSViewController).view.window?.delegate = self
}
func windowWillReturnUndoManager(window: NSWindow) -> NSUndoManager? {
println(undoManager)
return undoManager
}
But the undo and redo buttons are still grayed out when I open the sheet. Note that when change the segue style to popover, the undo/redo are working perfectly. How can I resolve this?
I had the same problem and am posting here in case it might help someone. My solution was to obtain the undo manager for the window to which the sheet view controller is attached:
let undoManager = self.view.window?.firstResponder?.undoManager
self in this context is the view controller for the sheet and not the parent view controller for the sheet. Therefore, the assignment would take place within the view controller for the sheet.

NSWindowController shows new window

I am very new to mac programming. Just started before 3 days.
I am making a sample app in which i have one button in main window
I am using this code to open a new wndowcontroller
ThirdViewController *tvc = [[ThirdViewController alloc] initWithWindowNibName:#"SecondViewController"];
[tvc showWindow:self];
This working fine but when i press button again it will open same window again so after every click i have +1 window on screen.
What i want is if my new window is already on my screen then button can't add same window.
Thanks in advance:)
If that code is being executed whenever the button is clicked then you’re effectively creating a new window controller, loading its window from a nib file, and showing that window as many times as the button is clicked.
The standard approach to prevent this from happening is having an instance variable that is initially nil and assigning it a window controller only once. Subsequently, the instance variable is not nil any longer and you can test that to avoid creating another controller and loading the nib file again.
You could, for example, declare the following instance variable in your application delegate or whatever controller should be responsible for the third window controller:
ThirdViewController *tvc;
and, when the button is clicked:
if (nil == tvc) {
// If tvc is nil then it's the first time this code is being executed
tvc = [[ThirdViewController alloc] initWithWindowNibName:#"SecondViewController"];
}
[tvc showWindow:self];

Resources