Can I observe an optional value in swift? If not, how might I go about attempting to observe a change? - swift

I'm trying to observe a change to the selection of an NSPopUpButton in Swift 4. In my view controller's viewDidLoad() I've set up the observation token to observe the selectedItem property of the NSPopUpButton
override func viewDidLoad() {
super.viewDidLoad()
observation = observe(\.myPopUpButton.selectedItem) {
objectToObserve, change in
if change.kind == NSKeyValueObservedChange.Kind.setting {
// code to execute goes here
}
}
I set a breakpoint on the line where observation is set to determine that the token is being configured with the correct key path. I also set a break inside the closure to see when it is executed. When I change the selection of the NSPopUpButton, the closure does not execute.
selectedItem is of type, NSMenuItem?, so my suspicion is that I can't set an observation on an optional property. But I can't find anything in Apple's documentation that states whether or not that is the case and I'm not sure how I would go about verifying it for myself.
So I have sort of a primary question along w/ some followups:
Can I observe an optional property in Swift 4.1?
If so, how can I troubleshoot this, what am I doing wrong?
If not, how can I go about trying to monitor the state of the NSPopUpButton?
Troubleshoots that I've already tried...
added #objc dynamic to the my myPopUpButton declaration

Many properties of many AppKit objects are not KVO-compliant. Unless the documentation specifically says the property is compliant, you should assume it's not compliant. NSPopUpButton's selectedItem property is non-compliant.
The easiest way to be notified that the pop-up button's selected item changed is to set the button's target and action:
override func viewDidLoad() {
super.viewDidLoad()
myPopUpButton.target = self
myPopUpButton.action = #selector(popUpButtonDidFire(_:))
}
#IBAction private func popUpButtonDidFire(_ sender: Any) {
// code to execute goes here
}
Note that if you're creating the pop-up button in a storyboard or xib, you can wire it to the popUpButtonDidFire method by control-dragging from the pop-up button to the view controller.

As mentioned in the comments in macOS Cocoa Bindings and Swift's property observers are a very powerful way to observe values, even in prior Swift versions. An outlet is not needed.
Create a property and use the didSet observer
#objc dynamic var selectedObject : MyObject? {
didSet {
}
}
In Interface Builder in Bindings Inspector bind Selected Object to the target controller Model Key Path > selectedObject.
MyObject is the type of the represented object of the menu item. If nothing is selected selectedObject is nil. You can bind also Selected Index, Selected Tag or Selected Value (but not simultaneously).

Related

Modify IBOutlet property from outside of viewDidLoad - Xcode Storyboards

I have a separate class which when called upon updates the ToolTip (a text property) for an NSButton in a pistonViewController via its IBOutlet.
However, whenever I try to perform the action, I get the error
"Unexpectedly found nil while implicitly unwrapping an Optional value"
since pistonViewController.piston.tooltip didn't work, I created an instance above the class:
let pistonView = pistonViewController();
and then from within the separate class called pistonView.set_piston();
func set_piston(index: Int) {
piston1.toolTip = "yay it worked!";
}
I get the same error: found nil.
How to get the correct instance of the pistonViewController (the one that appears on viewDidLoad) so that piston1 will not be nil?
There is this solution, but it looks needlessly complex. This one appears to only work on iOS, using a storyboard.InstantiateViewController command that does not work on MacOS. This MacOS solution is poorly explained and does not appear to work.
"[How do I] Modify IBOutlet property from outside of viewDidLoad"
(But what you're really asking is how you modify a view controller's views from outside of the view controller.)
The short answer is "Don't do that." It violates the principle of encapsulation. You should treat a view controller's view properties as private, and only modify them inside the view controller's code.
(To misquote Groucho Marx: "Doc, it crashes when I do this". "Then don't do that!")
Instead, add a public property (pistonToolTip) in your PistonViewController (Class names should begin with upper-case letters).
class PistonViewController: UIViewController {
var pistonToolTip: String {
didSet {
piston?.tooltip = pistonToolTip
}
}
}
And in case you set pistonToolTip before your PistonViewController has loaded its views, add this line to viewDidLoad:
override func viewDidLoad() {
super.viewDidLoad()
piston?.tooltip = pistonToolTip
// The rest of your viewDidLoad code
}
Ultimately I just set it up in viewDidLoad, with a timer waiting for the other program to get the variables that will then be assigned to the pistons.
The lack of effective pointers to instances of View Controllers makes anything else not possible or perhaps just arcane and difficult.

Swift macOS SegmentedControl Action not getting called

Description
I am trying to use NSSegmentedControls to transition between Child ViewControllers. The ParentViewController is located in Main.storyboard and the ChildViewControllers are located in Assistant.storyboard. Each ChildViewController has a SegmentedControl divided into 2 Segments and their primary use is to navigate between the ChildViewControllers. So they are set up as momentaryPushIn rather than selectOne. Each ChildViewController uses a Delegate to communicate with the ParentViewController.
So in the ParentViewController I added the ChildViewControllers as following:
/// The View of the ParentViewController configured as NSVisualEffectView
#IBOutlet var visualEffectView: NSVisualEffectView!
var assistantChilds: [NSViewController] {
get { return [NSViewController]() }
set(newValue) {
for child in newValue { self.addChild(child) }
}
}
override func viewDidLoad() {
super.viewDidLoad()
// Do view setup here.
addAssistantViewControllersToChildrenArray()
}
override func viewWillAppear() {
visualEffectView.addSubview(self.children[0].view)
self.children[0].view.frame = self.view.bounds
}
private func addAssistantViewControllersToChildrenArray() -> Void {
let storyboard = NSStoryboard.init(name: "Assistant", bundle: nil)
let exampleChild = storyboard.instantiateController(withIdentifier: "ExampleChild") as! ExampleChildViewController
let exampleSibling = storyboard.instantiateController(withIdentifier: "ExampleSibling") as! ExampleSiblingViewController
exampleChild.navigationDelegate = self
exampleSibling.navigationDelegate = self
assistantChilds = [exampleChild, exampleSibling]
}
So far so good. The ExampleChildViewController has an NSTextField instance. While I am in the scope of the TextField, I can trigger the action of the SegmentedControls. Its navigating forward and backward as it should. But once I leave the scope of the TextField I can still click the Segments, but they are not triggering any action. They should be able to navigate forward and backward even if the TextField is not the current "First Responder" of the application. I think I am missing something out here, I hope anyone can help me with this. I know the problem is not the NSSegmentedControl because I am seeing the same behavior with an NSButton, which is configured as Switch/Checkbox, in the SiblingViewController. I just don't have any idea anymore what I am doing wrong.
It`s my first time asking a question myself here, so I hope the way I am doing is fine for making progress with the solution. Let me know if I can do something better/different or if I need to provide more information about something.
Thanks in advance!
Additional Information
For the sake of completeness:
The ParentViewController itself is embedded in a ContainerView,
which is owned by the RootViewController. I can't imagine this does
matter in any way, but this way we are not missing something out.
I am actually not showing the navigation action, because I want to
keep it as simple as possible. Furthermore the action is not problem,
it does what I want it to do. Correct me if I am wrong with this.
Possible solutions I found while researching, which did not work for me:
Setting window.delegate of the ChildViewControllers to NSApp.windows.first?.delegate
Setting the ChildViewController to becomeFirstResponder in its func viewWillAppear()
visualEffectView.addSubview(self.children[0].view, positioned: NSWindow.OrderingMode.above, relativeTo: nil)
Related problems/topics I found while researching:
Basic segmented control not working
Adding and Removing Child View Controllers
NSSegmentedControl - Odd appearance when placed in blur view
How to set first responder to NSTextView in Swift?
How to use #selector in Swift 2.2 for the first responder
Accessing methods, actions and/or outlets from other controllers with swift
How to use Child View Controllers in Swift 4.0 programmatically
Container View Controllers
issues with container view
Control a NSTabViewController from parent View
How to detect when NSTextField has the focus or is it`s content selected cocoa
SOLUTION
let parentViewControllerInstance = self.parent as! ParentViewController
segmentedControl.target = parentViewControllerInstance
In my case I just had to set the delegate as the target of the sendAction method.
Background
Ok, after hours of reading the AppKit Documentation I am now able to answer my own question.
First, debugging the UI showed that the problem was definitely not in the ViewHierarchy.
So I tried to think about the nature of NSButton and NSSegmentedControl. At some point I noticed that both are subclasses of NSControl.
class NSSegmentedControl : NSControl
class NSButton : NSControl
The AppKit Documentation says:
Discussion
Buttons are a standard control used to initiate actions within your app. You can configure buttons with many different visual styles, but the behavior is the same. When clicked, a button calls the action method of its associated target object. (...) You use the action method to perform your app-specific tasks.
The bold text points to the key of the solution – of its associated target object. Typically I define the action of an control item like this:
button.action = #selector(someFunc(_:))
This causes the NSControl instance to call this:
func sendAction(_ action: Selector?, to target: Any?) -> Bool
Parameter Description from the documentation:
Parameters
theAction
The selector to invoke on the target. If the selector is NULL, no message is sent.
theTarget
The target object to receive the message. If the object is nil, the application searches the responder chain for an object capable of handling the message. For more information on dispatching actions, see the class description for NSActionCell.
In conclusion the NSControl instance, which was firing the action method (in my case the NSSegmentedControl), had no target to send its action to. So it was only able to send its action method across the responder chain - which obviously has been nil while the first responder was located in another view.

Get reference to current object

My goal is to safe a reference from the button, label or textfield inside a variable.
The problem is that I don't know on which control the user tapped.
I am having a simple application which looks like this:
The user can touch any control.
It is easy enough with just those three controls because I can drag in a action. But if I am having many of them I can't handle them all over the action methods. Is there a general way in which I can safe a reference to the control in a variable so that I can know which of the controls is the active one?
Edit
As suggested I am using a function and assigning the variable to the sender of the function. This is how it looks in code:
var currentObject: NSTextField!
override func viewDidLoad() {
super.viewDidLoad()
myTextfield.action = #selector(myAction)
}
func myAction(sender: NSTextField)
{
print("aktuell: \(sender)")
currentObject = sender
}
As you can see this only works for a NSTextfield. Is there a way in which the function works for every control?
Set the tag attribute for each item, and then you can check sender.tag to identify which object is calling it.
To set the tag, select the Attributes inspector in Storyboard (upper right side - middle button of Utilities) and look for this section:

NSCollectionView selection handling in Swift

Learning with Swift and I've been at this all day with little progress:
Need to know when an item in NSCollectionView is selected. The end goal is to get the item to highlight and to be able to delete it from the collection with the delete key. My NSCollectionView is bound to an ArrayController to get content and send the selection indexes, so looks like I need to be watching the ArrayController for a selection change, but don't see any helpful delegate methods there. The prototype view has a single textfield.
I was following the obj-c examples here and elsewhere (found none in Swift), but a Swift NSCollectionViewItem doesn't have the setSelected method to override. It has a selected property.
How to get informed when an NSCollectionViewItem gets selected in Swift?
The most simple solution is to override the selected property and react for example whenever it is set:
class CollectionSonaViewItem: NSCollectionViewItem {
override var isSelected: Bool {
didSet {
// set background color to indicate selection
self.view.layer?.backgroundColor = (isSelected ? NSColor.blue.cgColor : NSColor.clear.cgColor)
// do more stuff
}
}
From there on you can send a notification or call a function in your collection view class, its delegate or whatever required.

How can I use didSet to change the text of a UITextField (IBOutlet)?

I'd like to set text value of UITextField (IBOutlet) in the DidSet of my model object that I pass.
Here the code:
let manageSettingViewController: ManageSettingViewController = self.storyboard?.instantiateViewControllerWithIdentifier("ManageSettingViewController") as ManageSettingViewController
self.navigationController?.pushViewControllerCustom(manageSettingViewController)
manageSettingViewController.setting = setting
And in the didSet of manageSettingViewController:
var setting: Setting? {
didSet
{
keyTextField.text = setting?.label
valueTextField.text = setting?.value
}
How can I set the text? Because in this case Xcode crash because "keyTextField is nil" :(
You're setting manageSettingViewController.setting right after instantiating manageSettingViewController -- at this point, it hasn't loaded its view from the nib/storyboard yet, so all of its IBOutlet variables (which presumably keyTextField and valueTextField are) are still nil. Those text fields are hooked up as of when ManageSettingViewController's viewDidLoad method is called.
You could change your didSet to check the optional outlets before setting them, or assign through optional chaining:
didSet {
keyTextField?.text = setting?.label
valueTextField?.text = setting?.value
}
This would avoid the crash, but it would also fail to change your text field's content. You'd have to also implement viewDidLoad for ManageSettingViewController to check its setting property and set its text fields accordingly.
Of course, that would duplicate the code from your didSet. That code might still be useful if you want to set setting from elsewhere and have the UI update automatically, but didSet won't help you for updating UI before the UI loads.