UI Save/Restoration mechanism in Cocoa via Swift - macos

I'd like to save the state of Check Box, quit application, then launch macOS app again to see restored state of my Check Box. But there's no restored state in UI of my app.
What am I doing wrong?
import Cocoa
class ViewController: NSViewController {
#IBOutlet weak var tick: NSButton!
override func viewDidLoad() {
super.viewDidLoad()
}
override func encodeRestorableState(with coder: NSCoder) {
super.encodeRestorableState(with: coder)
coder.encode(tick.state, forKey: "")
}
override func restoreState(with coder: NSCoder) {
super.restoreState(with: coder)
if let state = coder.decodeObject(forKey: "") as? NSControl.StateValue {
tick.state = state
}
}
}

To the best of my knowledge, this is the absolute minimum you need to implement custom UI state restoration of a window and/or its contents.
In this example, I have a window with a checkbox and that checkbox's state represents some custom view state that I want to restore when the app is relaunched.
The project contains a single window with a single checkbox button. The button's value is bound to the myState property of the window's content view controller. So, technically, the fact that this is a checkbox control is irrelevant; we're actually going to preserve and restore the myState property (the UI takes care of itself).
To make this work, the window's restorable property is set to true (in the window object inspector) and the window is assigned an identifier ("PersistentWindow"). NSWindow is subclassed (PersistentWindow) and the subclass implements the restorableStateKeyPaths property. This property lists the custom properties to be preserved/restored.
Note: if you can define your UI state restoration in terms of a list of key-value compliant property paths, that is (by far) the simplest solution. If not, you must implement encodeRestorableState / restoreState and are responsible for calling invalidateRestorableState.
Here's the custom window class:
class PersistentWindow: NSWindow {
// Custom subclass of window the perserves/restores UI state
// The simple way to preserve and restore state information is to just declare the key-value paths
// of the properties you want preserved/restored; Cocoa does the rest
override class var restorableStateKeyPaths: [String] {
return [ "self.contentViewController.myState" ]
}
// Alternatively, if you have complex UI state, you can implement these methods
// override func encodeRestorableState(with coder: NSCoder) {
// // optional method to encode special/complex view state here
// }
//
// override func restoreState(with coder: NSCoder) {
// // companion method to decode special/complex view state
// }
}
And here's the (relevant portion) of the content view controller
class ViewController: NSViewController {
#objc var myState : Bool = false
blah, blah, blah
}
(I built this as a Cocoa app project, which I could upload if someone tells me where I could upload it to.)

Actually you don't have to go through restorableStateKeyPaths / KVO / KVC if you don't want to.
I was stuck in the same state as you with the encodeRestorableState() & restoreState() methods not being called but found out what was missing.
In System Preferences > General, make sure "Close windows when quitting an app" is unchecked.
Make sure that the NSWindow containing your view has "Restorable" behavior enabled in IB.
Make sure that your NSViewController has a "Restoration ID" set.
Your NSViewController won't be encoded unless you call invalidateRestorableState(). You need to call this each time there's a state in your NSViewController that changes and that you want to have saved.
When no state changes in the NSViewController after having restored it, its state would not be encoded again when closing the app. Which would cause the custom states to not be restored when relaunching the app. The simplest way I found is to also call invalidateRestorableState() in viewDidLoad(), so that state is always saved.
After doing all that, I didn't even have to additionally implement NSApplicationDelegate or NSWindowRestoration protocol methods. So the state restoration of the NSViewController is pretty self-contained. Only external property is restorable NSWindow.

After losing a couple of hours of my life to this problem I finally got it working. Some of the information in the other answers was helpful, some was missing, some was not necessary.
Here is my minimal example based on a new Xcode 13 project:
in AppDelegate add (this is missing in the other examples):
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { return true }
in ViewController add:
#objc var myState : Bool = false
override class var restorableStateKeyPaths: [String] {
return [ "myState" ]
}
set up some UI and bind it to myState to see what is going on
make sure System Preferences > General > "Close windows when quitting an app" is unchecked
Things that I did not need to do:
create a custom window subclass
set a custom restoration id
it worked fine just with Xcode start/stop

Related

How to observe changes in system preference "Sidebar icon size" in AppKit?

In the Main section of the System Preferences of macOS, the user can change the "Sidebar icon size". This setting changes the icon size (and since macOS 11 also the font size) used in Sidebars, when the size mode of the NSOutlineView in question is set to default.
I want to add an UserNotification observer in my app to track changes to the said setting, so I can perform some additional finetuning after each size change, but could not find any applicable NSNotification.Name or other constant.
Any workable solution or suggestion is welcome.
I knew I had a solution for this, but I was searching in the wrong projects. This is what I found. Observing UserDefaults is not a fantastic option, but at least it works:
fileprivate let tableViewDefaultSizeModeKey = "NSTableViewDefaultSizeMode"
override func viewDidLoad() {
super.viewDidLoad()
UserDefaults.standard.addObserver(self, forKeyPath: tableViewDefaultSizeModeKey, options: .new, context: nil)
}
deinit {
UserDefaults.standard.removeObserver(self, forKeyPath: tableViewDefaultSizeModeKey)
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == tableViewDefaultSizeModeKey {
// Do whatever is needed
}
}
Thanks for the code.
Please note that the changed value accessible in observeValue(forKeyPath... is the reliable way to access the new value. If one tries to access the effectiveRowSizeStyle in the table/outline view in this callback, the value has not yet been set in the view..and is still equal to the old value.
I also tried to call reloadData() from the observeValue(forKeyPath... callback to force a redraw of some cells that had some custom icon sizes, and found that event in the drawing code, the new updated rowSize was inaccessible. I ended up adding a custom ivar to my OutlineView to store the 'next' rowSize in order to properly size my icons in the redraw of the cells.

How to get state restoration to restore all windows/tabs on macOS?

I have a non-document based macOS AppKit app. It has one window instantiated automatically by the storyboard. I have sub-classed NSWindowController and added a override func newWindowForTab(_ sender: Any?) to enable the + button on the tab-bar. My main view controller lets the user rename the tab title and the window title is set to the same. This is kind of like how Xcode tab renaming works.
Additionally I have sub-classed NSWindow and added a restorableStateKeyPaths to ensure tab and window titles are automatically restored on app restart.
This all works great.
But only for the first tab. The main window is loaded and it has the tab and window titles set automatically.
The other tabs (windows) are not restored.
Any hints on what I miss to make all tabs restored?
My NSWindowController:
class MyWindowController: NSWindowController {
var subview: MyWindowController?
#IBAction override func newWindowForTab(_ sender: Any?) {
let story = self.storyboard
let windowVC = story?.instantiateInitialController() as! Self
window?.addTabbedWindow(windowVC.window!, ordered: .above)
subview = windowVC
windowVC.window?.orderFront(self.window)
windowVC.window?.makeKey()
}
}
My NSWindow:
class MyWindow: NSWindow {
override class var restorableStateKeyPaths: [String] {
return [ "self.tab.title", "self.title" ]
}
}
First you need to make sure that state restoration is enabled for your user, you can do this by going to Preferences->General and unchecking "Close windows when quitting an app".
Then you should use a restoration class in order to restore all open windows.
Basically if an NSWindow doesn't have a restoration class it won't be preserved across launches, that includes your storyboard loaded window. In this case what is happening is Cocoa is ignoring all window preservation because you haven't defined a restoration class for any of your windows so it resorts to its default behavior which is loading the initial storyboard controller.
Implementing restoration class is easy, just create a restoration class that inherits from NSObject and conforms to NSWindowRestoration, then implement its only required type method restoreWindow(identifier:state:completionHandler) like so:
class MyAppWindowRestoration: NSObject, NSWindowRestoration {
static func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier,
state: NSCoder,
completionHandler: #escaping (NSWindow?, Error?) -> Void) {
// 1.- Retrieve and show the window
// Retrieve a new instance of the only window
let window = (NSStoryboard.main?.instantiateInitialController() as? NSWindowController)?.window
// Call the completion handler with the window and no errors
completionHandler(window, nil)
}
}
Then just assign this class as the window restoration class on every window you want restored, you can do this everywhere after the window has loaded:
window.restorationClass = MyAppWindowRestoration.self
Unfortunately Apple's documentation on state restoration completely sucks so if you have any more questions let me know ;)

NSProgressIndicator on Modal Sheet Doesn't Animate

My main window controller has a toolbar item that triggers the presentation of a modal sheet. The sheet is supposed to display the progress of a lengthy, asynchronous process (e.g., sync local data with a server).
However, I can not get the (indeterminate) progress indicator to animate.
This is the action that triggers the modal sheet:
var syncProgressWindowController: SyncProgressWindowController!
// ...
#IBAction func syncWithServer(_ sender: AnyObject) {
// (Actual HTTP code not implemented)
syncProgressWindowController = SyncProgressWindowController()
syncProgressWindowController.loadWindow()
guard let modalWindow = syncProgressWindowController.window else {
return
}
self.window?.beginSheet(modalWindow, completionHandler: { (response) in
// THIS GETS EXECUTED.
// However, the code below has no effect:
self.syncProgressWindowController.progressIndicator.startAnimation(self)
// self.syncProgressWindowController.progressIndicator is
// NOT nil, despite windowDidLoad() not being called
// (see below)
})
}
The modal sheet window controller (class SyncProgressWindowController above) is defined like this:
#IBOutlet weak var progressIndicator: NSProgressIndicator!
convenience init() {
self.init(windowNibName: "SyncProgressWindow")
}
override func windowDidLoad() {
super.windowDidLoad()
// Breakpoints here don't work, logs don't print to the console.
// Not called? But outlet _is_ set (see above).
}
The xib file (SyncProgressWindow.xib) has:
File's Owner Identity/Class set to "SyncProgressWindowController"
Window has New Referencing Outlet to File's Owner's window
Window has delegate outlet wired to "File's Owner" (just in case - but delegate methods don't seem to get called either).
Window has "Visible at Launch" unchecked (and is therefore displayed modally with no problems).
Progress has New Referencing Outlet wired to File's Owner's progressIndicator.
However:
SyncProgressWindowController's windowDidLoad() does not get called (Execution does not stop at breakpoints there and logs aren't printed).
Despite that, the property/outlet progressIndicator is set somehow, because the app does not crash when I attempt to animate it, with code like this:
self.syncProgressWindowController.progressIndicator.startAnimation(self)
What am I missing?
completionHandler will be fired when you close sheet by endSheet(_:returnCode:) So you start indicator before sheet will be closed.
I'm not good in xib files, but when i disabled row with loadWindow, windowDidLoad was called. I'm not sure it's right way.

How do I access the undoManager for my Mac OS app in swift?

I am simply trying to get undo working for the actions a user performs in my app. By default, any text editing the user does has the benefit of undo, but any actions that are done otherwise (from my code) does not.
I can see the documentation explains that I need to get an instance of NSUndoManager and call registerUndoWithTarget, but I am stumped with the first step: getting the undoManager from within my ViewController. Since ViewController is a UIResponder, I tried this:
if let undoManager = self.undoManager {
undoManager.registerUndoWithTarget(self, selector: Selector("removeLatestEntry:"), object: "test")
}
Since that binding returns nil, I thought maybe the ViewController doesn't have the undoManager, so I looked for it in the window:
if let window = NSApplication.sharedApplication().mainWindow {
if let undoManager = window.undoManager {
undoManager.registerUndoWithTarget(self, selector: Selector("removeLatestEntry:"), object: "test")
}
}
Alas, the window binding also returns nil. Sorry, I am very new to this. Can anyone point me in the right direction? Am I supposed to implement my own undoManager or something? There is clearly an undoManager somewhere because anything a user does manually in my textField is getting undo behavior. It seems like this would be a singleton that I could access easily from a ViewController.
--
Edit: BTW, the code above was placed in viewDidLoad and removeLatestEntry is just a function in my ViewController that takes a string and prints it at this point.
To use undoManager, the ViewController needs to be first responder. So:
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
becomeFirstResponder()
}
Then, from wherever your action is defined that needs to be reversed, you register your undo and pass it whatever it needs to undo that action. So in my case:
func addEntry(activity: String) {
// some other stuff…
undoManager!.registerUndoWithTarget(self, selector: Selector("removeLatestEntry:"), object: activity)
}

How do I subscribe to TextField's TextChanged event in Xcode

I've recently started working on some test projects to get the feel for OS X development with Xcode. I come from Windows, so I might not be making much sense here.
How would I subscribe to certain "events" in Swift? I have just learned how to connect actions to UI objects. For example, I can now click a button, and change the text of a label programatically. However, and this may just be a case of lack of knowledge on my part - I am not able to find a way to subscribe to a TextField's "Text Changed" event.
Let's say that I have a TextField, and when I change the text at runtime (i.e. type something), I want to do something in the textChanged event for that particular TextField.
Is there even such a thing as a TextChanged event in OS X development?
Update
I am now using the following code:
import Cocoa
class ViewController: NSViewController {
class textField:NSTextField, NSTextFieldDelegate
{
override func awakeFromNib() {
delegate = self;
}
override func controlTextDidChange(obj: NSNotification)
{
println("Text changed.")
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
}
And I have added a ClassName to the TextField control in the Identity Inspector, but it isn't responding to the text changing. The message given is:
Failed to connect (textField) outlet from
(Xcode_Action_Basics.ViewController) to (NSTextField): missing setter
or instance variable
I just googled that error and came across this page: Failed to connect (storyboard) outlet from (NSApplication) to (NSNibExternalObjectPlaceholder) error in Cocoa and storyboard which states that this is a known issue in Xcode and that it does not mean there is a problem with your code - but I'm not so sure about that, because the code isn't working. Not sure if I've missed out on something.
Create a class that implements the protocol NSTextFieldDelegate like
class MyTextField:NSTextField, NSTextFieldDelegate {
override func awakeFromNib() {
delegate = self // tell that we care for ourselfs
}
override func controlTextDidChange(obj: NSNotification) {
// .... handle change, there are a lot of other similar methods. See help
}
}
In IB assign this class here:

Resources