NSViewController with/without nib - appkit

I’m adding an NSViewController subclass to a library and trying to have my cake and eat it too.
The default implementation of MyViewController() ( Or init() )
throws an exception
if it can’t find itself in a nib or storyboard. In order to overcome this I must override loadView() and create and assign a view in code. What I really want to do is both, create a view in the code, but only have that code run in the event that the super’s implementation doesn’t find one in a nib. I tried a
do {
super.loadView()
}
catch{
//viewCreatingCode
}
But unfortunately loadView is not marked as throws so the catch doesn’t work. Any ideas?

Ok, here's what you're going to want to do.
NSViewController has three properties you want to inspect: nibBundle, nibName, and storyboard. All three of these properties are optional, and they're set before loadView is called.
The default implementation of loadView checks to see if these properties are set. If so, it'll initialize the view from that nib or storyboard. Otherwise, it creates an empty NSView.
In your implementation of loadView, you'll want to check these values too. If nibName and storyboard are both nil, you can create and set up the view how you'd like.
override func loadView() {
guard nibName == nil, storyboard == nil else {
super.loadView()
return
}
view = MyCustomView()
}

Related

UI Save/Restoration mechanism in Cocoa via Swift

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

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:

OS X addsubview from xib in swift

I'm trying to add a new sub view form a nib using swift for OS X.
So far i've:
created a new "Cocoa Application"
added a new "Cocoa Class" called "TestSubView" as a subclass of NSViewController with a XIB file
I want to add this subview to my main view when the application loads.
in my ViewController ( the ViewController for the main window ) i have.
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let newSubView = TestSubView();
self.view.addSubview(newSubView.view);
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
}
But i'm getting the following error
Failed to set (contentViewController) user defined inspected property on (NSWindow):
-[NSNib initWithNibNamed:bundle:] could not load the nibName: temp.TestSubView in bundle (null).
I realise i will need to size and position this subview but I can't seem to get to that point.
I've spent the better part of a day trying to figure this one out so any help would be greatly appreciated.
I finally got this thing to work. My new code looks like
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let subview = TestSubView(nibName: "TestSubView", bundle: nil)!
self.view.addSubview(subview.view)
}
override var representedObject: AnyObject? {
didSet {
// Update the view, if already loaded.
}
}
}
Found with the help of the docs & this answer
It was suggested that if the nib name and the class name are the same you shouldn't need to declare nibname: (as i'd tried to do originally) but the docs didn't mention this - explains why it didn't work!
For prosperity, this worked for me with Xcode 6.1.1 on OS X Yosemite (10.10.1)
A nib is really nothing but an XML file with view information in it. You have to get it from the application bundle and get one of the views contained in it explicitly. You are perhaps confounding views and view controllers (your attempt to extract view from newSubView suggests that).
Try this:
let subview = NSBundle.mainBundle().loadNibNamed("TestSubView",
owner:self, options:nil)![0]! // maybe no final unwrapping "!" in Swift 3
self.view.addSubview(subview)
Make sure the xib is really called the name you are using and contains at a least one view (otherwise the two unwrapping ! above will crash your app).

Accessing controls of views of NSCollectionView

I'm using an NSCollectionView to display various objects. The whole things works rather well, except for one annoying thing. I cannot figure out how to access the various controls on the view used to represent each object in the collection.
Here's the setup:
I have dragged an NSCollectionView into my view in IB.
I made a custom subclass of NSCollectionViewItem. Mapped my class in IB.
I made a custom subclass of NSBox to act as the view for each object in the collection. Also mapped this class in IB and connected it to the view property of my NSCollectionViewItem subclass.
I made all the bindings in IB to display the correct information for each object.
The view:
The resulting collection view:
Reasoning that that my subclass of NSCollectionViewItem is basically a controller for each view in the collection, I made referencing outlets of the various controls in the view in my controller subclass:
#interface SourceCollectionViewItem : NSCollectionViewItem
#property (weak) IBOutlet NSTextField *nameTextField;
#property (weak) IBOutlet NSTextField *typeTextField;
#property (weak) IBOutlet RSLabelView *labelView;
#property (weak) IBOutlet NSButton *viewButton;
#end
When I inspect any instance of SourceCollectionViewItem in the debugger, all the properties show up as nil despite the fact that I can actually see them on my screen and that everything is displayed as it should be.
My setup was inspired by Apple's sample app IconCollection.
I am obviously missing something. What?
EDIT: I found various posts hinting at a similar issue:
CocoaBuilder.com and this question on SO.
EDIT: Just to be complete: this post deals with the subject as well and delivers a solution based on a combination of the options mentioned in the accepted answer.
Outlets are set during nib loading, and only the prototype item is loaded from nib and has its outlets assigned. All other ViewItems and their Views are cloned from the prototype, in that case outlets are just instance variables that are never initialized.
Here are the options I could come up with:
Override newItemForRepresentedObject: of collection view and reload nib instead of cloning the prototype. But this will probably hurt the performance greatly.
Override copyWithZone of collection view item and assign outlets manually using viewWithTag: to find them.
Give up and try to provide data via bindings only.
I found that overriding NSCollectionViewItem's -setRepresentedObject: could also be a good choice, as it is called on the new Item when all IBOutlet seem to be ready. After the call to super you can do whatever is needed:
- (void)setRepresentedObject:(id)representedObject
{
if (representedObject) {
[super setRepresentedObject:representedObject];
[self.anOutlet bind:#"property" toObject:self.representedObject withKeyPath:#"representeProperty" options:nil];
}
}
I used this method to bind a custom property of an interface object. The check is there to avoid useless calls, when the representedObject is not yet ready. The project uses a separate xib for the ViewItem, as explained in the links in the original edits.
Great question. Like #hamstergene suggests, you can use copyWithZone, it will be much more efficient compared to newItemForRepresentedObject. However viewWithTag is not always an option, first, because not everything can be tagged (easily), and, second, using tag for this purpose is a little wrong. Here's a cool approach with performance in mind, in Swift.
import AppKit
class MyViewController: NSCollectionItemView
{
// Here you are cloning the original item loaded from the storyboard, which has
// outlets available, but as you've seen the default implementation doesn't take
// care of them. Each view has a unique identifiers, which you can use to find it
// in sublayers. What's really cool about this, is that you don't need to assign
// any tags or do anything else while having advantage of better performance using
// cached nib object.
override func copyWithZone(zone: NSZone) -> AnyObject {
let copy: NSCollectionItemView = super.copyWithZone(zone) as! NSCollectionItemView
let oldView: RecordingView = self.view as! MyView
let newView: RecordingView = copy.view as! MyView
newView.foo = newView.viewWithIdentifier(oldView.foo.identifier!) as! NSTextfield
newView.bar = newView.viewWithIdentifier(oldView.bar.identifier!) as! NSImageView
return copy
}
}
#IBDesignable class MyView: View
{
// Custom collection view item view. Lets assume inside of it you have two subviews which you want
// to access in your code.
#IBOutlet weak var foo: NSTextfield!
#IBOutlet weak var bar: NSImageView!
}
extension NSView
{
// Similar to viewWithTag, finds views with the given identifier.
func viewWithIdentifier(identifier: String) -> NSView? {
for subview in self.subviews {
if subview.identifier == identifier {
return subview
} else if subview.subviews.count > 0, let subview: NSView = subview.viewWithIdentifier(identifier) {
return subview
}
}
return nil
}
}

Resources