Get values from NSTextField, NSComboBox in a unified way? - macos

I have the following method set as the action of an NSComboBox and two NSTextFields:
- (IBAction)valueChanged:(id)sender
{
if (sender == comboBox) {
[myModel setFoo1:[comboBox intValue]];
} else if (sender == intTextField) {
[myModel setFoo2:[intTextField intValue]];
} else if (sender == floatTextField) {
[myModel setFoo3:[floatTextField floatValue]];
}
}
I wondered if I could make that method into something like this, in order to increase maintainability:
- (IBAction)valueChanged:(id)sender
{
[myModel setValue:[sender value] forKey:[sender identifier]];
}
Unfortunately, it doesn't work like that. I get the following error:
[NSComboBox value]: unrecognized selector sent to instance 0x7fed42029430
How do I get the values from the controls in a uniform way (i.e. as an object), no matter if int or float? The KVC setValue:forKey: method I use on my model should be able to deduce the actual type of the value object (i.e. the kind of NSNumber in this case) just fine, right?
Or is this not possible at all? (I know I could probably use bindings to bind the UI controls to the model's value fields, but that's not what I want to do.)

Since both NSTextField and NSComboBox are subclasses of NSControl, you should be able to use -(id)objectValue to get the control's value (or selected value) in a uniform way.

Related

Setting a custom Id to a NSWindow

I am creating a multi-window application on macOS. Each window created by me is a NSWindow. I would like to assign a custom Id as per my need, to the created NSWindow so that I can later find the window using the Id. Is there a way to do it ?
I understand there is a property called windowNumber. However, one cannot set this to a value as they desire.
var windowNumber: Int { get }
Thanks in Advance.
You can use the identifier property of a NSWindow object to assign a custom id string to a window, like this:
let myWindowIdentifier = NSUserInterfaceItemIdentifier("My window identifier")
window.identifier = myWindowIdentifier
To find a window using this identifier, you use code like this:
for window in NSApp.windows {
if window.identifier == myWindowIdentifier {
// Do something
break
}
}

Handling NSMenuDelegate menuWillOpen for changing targets

There are lots of related answers about using menuWillOpen. They all explain that one needs to set the menu's delegate first.
This is easy when I have just one target, like a Preferences window or the main application.
But what if I have a document based app, and I need to have the active document handle menuWillOpen? Then the delegate isn't a constant any more.
What's the proper way to handle this? Do I have to set the delegate to a single object (like the AppDelegate) and then forward the call to the active view controller (but how is that done correctly)? Or is there some other elegant way?
I came up with this code which appears to work:
// This is in my AppDelegate class, and the NSMenu's delegate points to it:
- (void)menuWillOpen:(NSMenu *)menu {
// Forward to active document controller
NSWindow *mainWindow = [NSApplication sharedApplication].mainWindow;
NSResponder *r = mainWindow.firstResponder;
while (r) {
if ([r respondsToSelector:_cmd]) {
[(id<NSMenuDelegate>)r menuWillOpen:menu];
return;
}
r = r.nextResponder;
}
}
It assumes that a controller down the responder chain implements menuWillOpen:

NSView with a KVC property in Swift

I have a custom NSView class defined as:
class MyView: NSView
{
var someText: NSString
override func didChangeValueForKey(key: String)
{
println( key )
super.didChangeValueForKey( key )
}
// other stuff
}
What I want to be able to do is from outside of this class change the value of someText and have didChangeValueForKey notice that someText has changed so I can, for example, set needsDisplay to true for the view and do some other work.
How an I do this?
Are you sure you need KVC for this? KVC works fine in Swift, but there’s an easier way:
var SomeText: NSString {
didSet {
// do some work every time SomeText is set
}
}
There is no KVC mechanism for this because this isn't what KVC is for.
In Objective-C, you would implement the setter explicitly (or override if the property is originally from a superclass) and do your work there.
In Swift, the proper approach is the didSet mechanism.
didChangeValueForKey() is not part of KVC, it's part of KVO (Key-Value Observing). It is not intended to be overridden. It's intended to be called when one is implementing manual change notification (as a pair with willChangeValueForKey()).
More importantly, though, there's no reason to believe that it will be called at all for a property which is not being observed by anything. KVO swizzles the class in order to hook into the setters and other mutating accessors for those properties which are actually being observed. When such a property is changed (and supports automatic change notification), KVO calls willChangeValueForKey() and didChangeValueForKey() automatically. But for non-observed properties, those methods are not called.
Finally, in some cases, such as the indexed collection mutation accessors, KVO will use different change notification methods, such as willChange(_:valuesAtIndexes:forKey:) and didChange(_:valuesAtIndexes:forKey:).
If you really don't want to use didSet for some reason, you would use KVO to observe self for changes in the someText property and handle changes in observeValueForKeyPath(_:ofObject:change:context:). But this is a bad, clumsy, error-prone, inefficient way of doing a simple thing.
KVO and didSet are not mutually exclusive:
import Foundation
class C: NSObject {
dynamic var someText: String = "" {
didSet {
print("changed to \(someText)")
}
}
}
let c = C()
c.someText = "hi" // prints "changed to hi"
class Observer: NSObject {
init(_ c: C) {
super.init()
c.addObserver(self, forKeyPath: "someText", options: [], context: nil)
}
override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
print("observed change to \(object!.valueForKeyPath(keyPath!))")
}
}
let o = Observer(c)
c.someText = "test" // prints "changed to test" and "observed change to test"
I would add to Jaanus's answer that to make the property KVC compliant, you should declare it as dynamic var someText: NSString.
But if you don't need all the bells and whistles oh KVC, didSet is the way to go.
Update
As for didChangeValueForKey: – it is intended for the opposite, for you to notify value for key has changed (if it is not due to one of the cases covered by Foundation). You should use addObserver(_:forKeyPath:options:context:) and override observeValueForKeyPath(_:ofObject:change:context:) to be notified of changes.
Alternatively you can use one of many 3rd party solutions such as ReactiveCococa or Facebook's KVOController

control p5 slider.setRange() not found in subclass

Im trying to set the Range of a slider within a subclass, catching the respective controller using getController, which works fine, proven by the returned value i get within the print. But controller.setRange() doesnt get recognized as a function.
can the range only be initialized during the creation of the object or does getController return a different object than i expect it does?
thanks!
class Stepper
{
String stepperName = "default";
int ID = motorID;
int stepperValue;
Stepper(String givenName) {
stepperName = givenName;
cp5.addSlider(stepperName)
.setPosition(sliderPosX+(sliderWidth+100)*surgeonBotID, sliderPosY+90*servoList.length+30*(motorID-servoList.length))
.setSize(sliderWidth, int(sliderHeight*0.5))
//.setRange(0, 179)
.setSliderMode(Slider.FLEXIBLE);
println("Created new Stepper: "+stepperName+ " ID: "+ID);
motorID++;
}
void setRange(float min, float max){
println("object: "+cp5.getController(getStepperName()).getValue());
cp5.getController(getStepperName()).setRange(min, max);
}
...
}
Questions like these are best answered by consulting the API.
The getController() method returns a Controller. The Controller class does not have a setRange() function. That instance happens to be an instance of Slider, which is a subclass of Controller, but the compiler has no way of knowing that. That's what's causing your error.
You can tell the compiler that the instance is indeed a Slider by casting the returned value to a Slider, and then you can access the methods defined by the Slider class:
((Slider)cp5.getController(getStepperName())).setRange(min, max);
To make that easier to understand, here it is split up into two lines:
Slider s = (Slider)cp5.getController(getStepperName());
s.setRange(min, max);

An NSArrayController changes its selection : what is the best way to catch this event?

One can put an observer on the selectedIndex method of NSArrayController. This method has some drawbacks I think :
what will happen when the arrangedObjects is rearranged ? I admit this is not a very important problem
if we ask the observer to remember the old value of selectedIndex, it doesn't work. It is known but I cannot find again the link.
Why doesn't NSArrayController have a delegate ?
Is there another way to achieve what I want to do : launching some methods when the selection changes ?
Observe selection key of the NSArrayController (it is inherited from NSObjectController).
It will return either NSMultipleValuesMarker (when many objects are selected), NSNoSelectionMarker (when nothing is selected), or a proxy representing the selected object which can then be queried for the original object value through self key.
It will not change if rearranging objects did not actually change the selection.
You can also observe selectedObjects; in that case you won't need to deal with markers.
Providing hamstergene's excellent solution, in Swift 4.
In viewDidLoad, observe the key path.
arrayController.addObserver(self, forKeyPath: "selectedObjects", options: .new, context: nil)
In the view controller,
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
guard let keyPath = keyPath else { return }
switch keyPath {
case "selectedObjects":
// arrayController.selectedObjects has changed
default:
break
}
}

Resources