How to exclude certain AppKit views from restorable NSWindow? - macos

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

Related

How to detach NSTextView's undo manager from NSDocument?

I have an NSDocument that is in a non-directly editable format, it's a description of something in XML. The NSWindow has a settings view controller associated with it which manipulates the data in the document just fine, undo works as expected, save, etc. Now also in the NSWindow is an NSTextView, which the user can enter some text into, but is not part of the content of the document, it's used only as temporary text. Of course I want to support undo for this text too, so I have the "Undo" checkmark enabled in Interface Builder for this NSTextView, and undo works just fine.
Now comes the rub: the NSDocument is getting marked as dirty when the NSTextView is modified. Because this is temporary text, I don't want the user to be nagged to save changes to the document, that really are not part of the document.
How do I detach the NSTextView from the responder chain leading up to the NSDocument's undo manager instance? Simply providing a new instance of NSUndoManager doesn't solve it, because it just goes up the responder chain to NSDocument as well.
extension InputViewController: NSTextViewDelegate {
func undoManager(for view: NSTextView) -> UndoManager? {
return myUndoManager
}
}
I'm pretty sure you'd need to override the undoManager property of the window's view controller, not the text field's delegate.
However, to simply make your document/window permanently non-editable all you need to do is override either the documentEdited property so it always returns false, or override updateChangeCount so it ignores requests to record changes.
Thanks to James Bucanek's comment about overriding updateChangeCount, I was able to do something like this in my NSDocument subclass:
override func updateChangeCount(_ change: NSDocument.ChangeType) {
if !suppressChangeCount {
super.updateChangeCount(change)
} else {
suppressChangeCount = false
}
}
Thus allowing undo/redo to work in my InputViewController keycode handler without dirtying the document.

NSSplitView Collapse Event

I'm trying to implement an NSSplitView similar to Xcode where you can collapse a view by dragging its handle to under half its width and it will collapse automatically. I have that part working, but I need to update the state of a button in the toolbar when this happens. I tried listening to splitViewDidResizeSubviews and checking if the splitView's view is collapsed, but that method fires 16 times with collapsed == true, so I don't think I want to update the button's state 16 times. Is there a cleaner way to do this? I'm new to Cocoa, but from what I've seen, I would expect there to be some way to just say bind this button's state to the isCollapsed property and be done with it. Does such a thing exist?
If you subclass your NSSplitViewController you can add a listener for the SplitViewItem's isCollapsed property:
class MySplitViewController: NSSplitViewController {
var observer: NSKeyValueObservation?
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
let sideViewSplitViewItem = splitViewItems[0]
observer = sideViewSplitViewItem.observe(\.isCollapsed, options: [.initial, .new]) {splitViewItem, _ in
print("Sidebar collapsed state changed to: \(splitViewItem.isCollapsed)")
}
}
}
The best way to see what bindings are available is to check the docs, specifically the Cocoa Bindings Reference (look in the sidebar for the view you're after).
NSSplitView doesn't have the binding you describe, so I think you're on the right track with your current approach. Of course, you don't need to update the button's state sixteen times, just check it's value each time, and update it if needs be. It might seem a bit wasteful, but checking the value of a bool is a very cheap operation, and you won't notice any kind of performance hit.
While NSSplitView has no event or observable property for when one of it's subviews is "collapsed", the immediate subview itself will have its hidden property set to YES.
So you can either observe the subview's hidden property yourself, or if you're creating your own custom subview of NSView, you can override the -(void) setHidden:(BOOL)hidden to catch the event.
I do the latter, and it works correctly every time.
-(void)setHidden:(BOOL)hidden
{
[super setHidden:hidden];
// Insert code to react to the "collapsed" event here.
// (You're likely going to tell your parent view,
// self.window.windowController, or something.)
}
Of course the setHidden method / hidden property can in theory be called for purposes other than from NSSplitView "collapsing" your view, but this is very unlikely to ever happen in practice.

NSCollectionView does not hide in osx

I used NSCollectionView in my application. In certain condition i want to hide and Unhide the NSCollectionView. But it will not hide the NSCollectionView.
I used the following code
#IBOutlet weak var thumbnailView: NSCollectionView!
func applicationDidFinishLaunching(aNotification: NSNotification) {
thumbnailView.hidden = true
NSThread.sleepForTimeInterval(5)
thumbnailView.hidden = false
}
Note: Sleep the thread just for demonstration purpose
Also Hiding the NSScrollView is not working.
EDIT: I perform same code on Button Touch up Inside event i get the same result. It does not hide my CollectionView.
#IBAction func ButtonnISClick(sender: AnyObject) {
thumbnailView.hidden = true
NSThread.sleepForTimeInterval(5)
thumbnailView.hidden = false
}
Here are things that are wrong with your code (mostly because you did not provide enough information in your question):
Use lowercase strings when naming variables (so change ThumbnailView to thumbnailView;
I don't know where are you writing this code. Is it NSWindowController, NSViewController or NSWindow subclass? Depending on the location, you should write 2nd to 4th lines in different methods (either windowDidLoad(), viewDidLoad() or awakeFromNib()
Update: Given your code is in application did finish launching and you are sleeping in that method, you won't see any changes, since the window of your app is presented only after the method has returned (i.e. all code has been executed). I suggest you to move this code into a subclass of either NSWindowController's windowDidLoad (I'm not sure about that one) or NSViewController's viewDidAppear: method.

windowDidLoad() never called using storyboards for mac

I've just played around with the new mac storyboard-feature included in Xcode 6. I've set up a new OS X-project using storyboards and swift, then I've created a new file MainWindowController.swift, created the initializer init(coder: NSCoder!) (because otherwise the compiler warns me) and hooked everything up in the Main.storyboard file (set the MainWindowController-class for the WindowController in the inspector).
Everything compiles fine, my Window with the specified window content-view opens. But the code I've written in the windowDidLoad-function is never be called. Let it just something like:
override func windowDidLoad() {
super.windowDidLoad()
println("Executed")
}
I've also tested if my initializer is called - it is.
Does anybody has a clue? I've never used storyboards intensively on iOS before, maybe I miss something substantial.
In Yosemite, NSViewController has been promoted with powerful new features to make it work with Storyboards. Meanwhile, NSWindowController got demoted. With Storyboards, windows are no longer loaded from a nib, so windowDidLoad() doesn't get called anymore.
It makes sense for the window itself to become less important, in favor of a more powerful view it actually contains. My other answer on this page shows how to set up an AppDelegate to customize the window appearance. There's more detail on another page here, about using an AppDelegate to implement some of the things you might previously have done in an NSWindowController.
However, if you only wanted to catch windowDidLoad() as a way to customize the appearance options of the window, it is very easy to do that in Interface Builder, by simply adding them as User Defined Runtime Attributes to the NSWindow object. You don't need to subclass NSWindowController or write any code at all. Just plug in these values to the NSWindow object via the Identity Inspector pane to achieve the same effect shown in the AppDelegate example code:
Keypath: titlebarAppearsTransparent, Type: Boolean, Value: Checked
Keypath: titleVisibility, Type: Number, Value: 1
Keypath: styleMask, Type: Number, Value: 32783
Look in the headers to determine the actual numeric values of the constants.
( for example: NSWindowTitleVisibility.Hidden = 1 )
Of course, you can't specify individual bits of the styleMask, but it's easy enough to add them all together and get a single number to specify the style.
Remove everything except the Application scene from the Main.storyboard file, and instead, create a new Application.storyboard for the application window. Implement an application delegate class and connect it to the Application object in Main.storyboard. Use this class to instantiate the window controller and set up custom options for the application window.
class AppDelegate: NSObject, NSApplicationDelegate {
func applicationDidBecomeActive(notification: NSNotification) {
let storyboard = NSStoryboard(name: "Application", bundle: nil)
applicationController = storyboard.instantiateInitialController() as? NSWindowController
if let window = applicationController?.window {
window.titlebarAppearsTransparent = true
window.titleVisibility = NSWindowTitleVisibility.Hidden
window.styleMask |= NSFullSizeContentViewWindowMask
applicationController!.showWindow(self)
}
}
}
Rather than subclassing NSWindowController, use the ViewController.swift subclass of NSViewController that Xcode creates for you automatically with the project.
In the storyboard, notice how there's a Relationship that connects the "window content" to the ViewController. So, the ViewController can now do things that you might previously have done in a window controller.
The ViewController.swift file will already have a default override of viewDidLoad() that will be called when the window loads, just as you were expecting windowDidLoad() to be called if it were an NSWindowController subclass.
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
println("Executed")
}

NSTextField - notifications when individual keys are pressed

I am making an app that will add sound to keypresses as the user types in an NSTextField. I need to capture keystrokes and know what each individual keypress is (like "d" or "space" or "6"). The app depends on this. There is no other way around it.
Each window is an NSDocument File Owner, and it has a single NSTextField in it, which is where the document data is parsed, and the user will type.
After hours of parsing the Internet for answers and hacking away at code, the four most commonly repeated answers are:
"that is not how things work, here is (irrelevant answer)"
"you are new to Cocoa, that is a bad idea, use control:textView:doCommandSelector:" that doesn't give me individual keys, and some keys need their own unique sound trigger.
"use controlTextDidChange: or textView:shouldChangeTextInRange:replaceString:" controlTextDidChange doesn't give me individual keys, and the second one only works for textViews or UIKit.
People get confused and answer with recommendations for UIKit instead of AppKit, which is iOS-only.
The weird thing is that if I subclass NSTextField, it receives -keyUp. I don't know where -keyDown is going.
So my ultimate question is: can you tell me some kind of step-by-step way to actually capture the keyDown that is sent to NSTextField? Even if it's a hack. Even if it's a terrible idea.
I would love to solve this problem! I am very grateful for your reading.
controlTextDidChange is quite a good solution, but don't forget this 2 important things:
Set the delegate binding of the textField to the object where you define the controlTextDidChange method. Commonly, in document based apps it is the window controller, otherwise your app delegate.
Set the textField's control to "continous" in the attribute inspector section
If you miss those points, you will have no result.
This is a pretty old question, but as I was trying to implement a NSTextField that could react to keyDown so that I could create a hotkey preferences control I found I wanted the answer to this question.
Unfortunately this is a pretty non-standard use and I didn't find any places that had a direct answer, but I've come up with something that works after digging through the documentation (albeit in Swift 4) and I wanted to post it here in case it helps someone else with a non-standard use case.
This is largely based off of the information gleaned from the Cocoa Text Architecture Guide
There are three components to my solution:
Creating your NSWindowController and setting a NSWindowDelegate on your NSWindow:
guard let windowController = storyboard.instanciateController(withIdentifier:NSStoryboard.SceneIdentifier("SomeSceneIdentifier")) as? NSWindowController else {
fatalError("Error creating window controller");
}
if let viewController = windowController.contentViewController as? MyViewController {
windowController.window?.delegate=viewController;
}
Your NSWindowDelegate
class MyViewController: NSViewController, NSWindowDelegate {
// The TextField you want to capture keyDown on
var hotKeyTextField:NSTextField!;
// Your custom TextView which will handle keyDown
var hotKeySelectionFieldEditor:HotKeySelectionTextView = HotKeySelectionTextView();
func windowWillReturnFieldEditor(_ sender: NSWindow, to client: Any?) -> Any? {
// If the client (NSTextField) requesting the field editor is the one you want to capture key events on, return the custom field editor. Otherwise, return nil and get the default field editor.
if let textField = client as? NSTextField, textField.identifier == hotKeyTextField.identifier {
return hotKeySelectionFieldEditor;
}
return nil;
}
}
Your custom TextView where you handle keyDown
class HotKeySelectionTextView: NSTextView {
public override func keyDown(with event: NSEvent) {
// Here you can capture the key presses and perhaps save state or communicate back to the ViewController with a delegate pattern if you prefer.
}
}
I fully admit that this feels like a workaround somewhat, but as I am experimenting with Swift at the moment and not quite up to speed with all of the best practices yet I can't make an authoritative claim as to the "Swift-i-ness" of this solution, only that it does allow a NSTextField to capture keyDown events indirectly while maintaining the rest of the NSTextField functionality.
Try like this if you print nslog you will get individual character record for example you pressd "A" you will get the same in console:-
-(void)controlTextDidChange:(NSNotification*)obj
{
NSLog(#"%#",[yourTextfield stringValue]);
}
Also, not sure this is only your requirement.
Text editing for an NSTextField is handled by an NSTextView provided by the window, called the field editor. See the NSWindow method fieldEditor:forObject: and the NSWindowDelegate method windowWillReturnFieldEditor:toObject:. I suppose you could use one of these to provide your own subclassed NSTextView as the field editor. Or, could you simply use NSTextView instead of NSTextField?

Resources