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.
Related
I have a UICollectionView in my class declared as
#IBOutlet weak var artworkCollectionView: UICollectionView!
Inside this class there is one delegate method called by two other View Controllers, one of these VC is a pop up, the other one is a normal VC.
The delegate method gets some data from the database and then updates the collection view calling inside a closure:
self.artworkCollectionView.reloadData()
When the delegate method is called by the pop up VC, then all works great. BUT when the delegate method is called by the normal VC when it gets to self.artworkCollectionView.reloadData() it gets the infamous Fatal error: Unexpectedly found nil while implicitly unwrapping an Optional value.
I have checked all the references to the cell reuseIdentifier and all is correct. I suspect that since the UICollectionView is declared as weak var, when I go from the current class to the pop up and then the pop up calls the delegate methods, the reference is not lost, but when I go from the current class to the normal VC and then the normal VC calls the delegate method the reference to my weak var is lost and so it is "seen" as nil.
#IBOutlet weak var artworkCollectionView: UICollectionView!
override func viewDidLoad() {
super.viewDidLoad()
// Set up
artworkCollectionView.dataSource = self
artworkCollectionView.delegate = self
artworkCollectionView.isUserInteractionEnabled = true
artworkCollectionView.allowsSelection = true
artworkCollectionView.register(UINib(nibName:
"MyCollectionViewCell", bundle: nil),
forCellWithReuseIdentifier: "cell")
}
// delegate method
func reloadCollections() {
retrieveAlbumRatings { (isAlbum) in
if isAlbum {
self.retrieveAlbumData(completion: { (isFinished) in
if isFinished {
// Reload collection views
self.artworkCollectionView.reloadData()
}
})
}
}
}
If I am right, my question is: how can I give weak var artworkCollectionView: UICollectionView! a STRONG reference so that it does not get lost in the flow from the current class to the normal VC and back?
EDIT: here is what I tried so far:
Remove “weak” from the outlet declaration so making it: #IBOutlet var artworkCollectionView: UICollectionView!
But I got the same error
I passed artworkCollectionView to the normal VC via override performSegue and then passed it back as an argument of the delegate method. This does not give me the fatal error but also it does not reload the UICollectionView because I think that anyway the weak reference to the UICollectionView outlet is lost.
Thanks for your help (disclaimer: I am pretty new to Swift..)
Inside this class there is one delegate method called by two other
View Controllers, one of these VC is a pop up, the other one is a
normal VC.
The delegate method gets some data from the database and then updates
the collection view calling inside a closure:
self.artworkCollectionView.reloadData()
The flow appears to be that you have a VC containing the code above, the VC can either open a pop-up or just do a standard push segue to the "normal VC".
You want some operation to occur in either the pop-up VC or normal VC, load some data and then when the user is directed back to the originating VC, the UICollectionView is updated with that data.
Your problems are the following:
I passed artworkCollectionView to the normal VC via override
performSegue and then passed it back as an argument of the delegate
method. This does not give me the fatal error but also it does not
reload the UICollectionView because I think that anyway the weak
reference to the UICollectionView outlet is lost.
You shouldn't be passing anything around like this in most cases unless you have a really good reason to do so (I don't see one).
You want a separation of concerns here. You have to think carefully about what you wanjt to pass between VCs to avoid making weird dependencies between them. I wouldn't pass outlets for multiple reasons, the first being that you now have to keep track of the outlet in multiple VCs if you ever decide to change it. The second being that it requires too much mental gymnastics to keep track of the state of the outlet since it's being passed around everywhere. The outlets are also only guaranteed to be set at certain phases of the lifecycle. For example if you retrieve the destination VC from the segue in prepareForSegue:sender: and attempt to reference the outlets at that time, they will all be nil because they haven't been set yet.
These are all good reasons why the VC that contains the code above should be the one (and the only one) maintaining control over what gets displayed in artworkCollectionView and when. The problem here is how you're approaching this, rather than having the pop-up or normal VC call the delegate method or doing weird things like passing outlets from one VC to another, just pass the data around instead.
The simplest example is:
The pop-up VC and normal VC call some code to actually fetch the
data.
Then depending on how you actually segued to the pop-up VC or
normal VC from original VC, use either parentViewController or
presentingViewController to get the reference to the original VC.
Set the data into the original VC through that reference.
Dismiss the pop-up VC or normal VC if necessary (depends on your specific app, maybe you want the user to push a UIButton to dismiss instead of doing it for them).
When the original VC comes back into view, add some code to a lifecycle method like
viewWillAppear to have it load the contents of the data into the
UICollectionView at that time.
I see no reason why you should be passing any outlets outside of the original VC that should be the one managing it.
Short answer: Don't do that. You should treat a view controller's views as private. You should add a method to your view controller that other objects call to tell it to reload it's collection view.
The longer answer is that your view controller's collection view should stick around as long as the view controller is on-screen. It sounds like you don't have a very strong understanding of object lifecycle and how ARC works. You should read up on that and do some exercises until you understand it better.
Try something like this:
//Make artworkCollectionView a normal weak var, not implicitly unwrapped.
//You'll need to change your other code to unwrap it every time you use it.
#IBOutlet weak var artworkCollectionView: UICollectionView?
...
func reloadCollections() {
retrieveAlbumRatings { (isAlbum) in
if isAlbum {
//The construct `[weak self]` below is called a capture list
self.retrieveAlbumData(completion: { [weak self] (isFinished) in
guard let weakSelf = self else {
print("self is nil");
return
}
}
if isFinished {
// Reload collection views
guard let collectionView = weakSelf.artworkCollectionView else {
print("collectionView is nil!")
return
}
collectionView.reloadData()
})
}
}
}
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.
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).
I have a document window that contains a number of NSView subclasses, switched between using a tab control. Each of the subclasses, and the window's ViewController, support different user actions accessed through menu items tied to the First Responder.
I'd like to perform a segue from one of those views in response to a menu item. However, NSView does not support performSegueWithIdentifier, it appears to be something that is part of NSViewController alone.
Can someone suggest a way around this? I have seen suggestions to pass the VC into the views, but I am not clear how to do that. Or perhaps there is a better way?
view.containingController.performSegue()
note: you have to add containingController to your views
I WOULD add the viewController to the responder chain and then make containingController a computed property in an extension!
e.g. add vc as responder:
override func awakeFromNib() {
super.awakeFromNib()
self.nextResponder = self.view
for subview in self.view.subviews {
subview.nextResponder = self
}
}
e.g. containingController in extension
extension NSView {
var containingController: NSViewController? {
get {
while(self.nextResponder != nil) {
if(self.nextResponder is NSViewController) {
return self.nextResponder
}
}
return nil
}
}
}
You could do that (see Daij-Djan's answer), however it is not what I would recommend, since a hypothetical programmer who will be using your code, but is not familiar with it (let's say, you in a year :) ) might be caught by surprise by such behaviour.
I would recommend you to add a delegate (conforming to your custom protocol, let's call it MyViewDelegate) to your NSView with a method like viewRequiresToPerformTransition(view: YourViewSubclass). Then you implement this method (more generally, you conform to MyViewDelegate protocol) in your view controller and inside its implementation perform any segue you want.
First of all I have to say I am really new to swift and Objective C.I am learning them by myself.
I have a question for this code
I have a delegate in my SettingViewController called "settingsViewControllerFinished" and it pass the whole controller as a variable.
the code like this:
in my SettingViewController.swift
protocol SettingViewControllerDelegate: class {
func settingsViewControllerFinished(settingsViewController: SettingsViewController)
}
#IBAction func close(sender: AnyObject) {
dismissViewControllerAnimated(true, completion: nil)
self.delegate?.settingsViewControllerFinished(self)
}
I am confused.What did you mean if you pass the whole controller as a variable?(maybe the question is silly for you)
in my viewController:
func settingsViewControllerFinished(settingsViewController: SettingsViewController)
{
self.brushWidth = settingsViewController.brush
self.opacity = settingsViewController.opacity
self.red = settingsViewController.red
self.green = settingsViewController.green
self.blue = settingsViewController.blue
}
I guess the reason is:I pass everything in SettingViewController to ViewController so that I could use the variables in SettingViewController.
Am I rihgt?
Generally you are correct, yes: passing the SettingViewController back to its delegate enables the original caller to not have to keep a reference to the created and shown SettingViewController since the delegate method sends the relevant information along already.
But there is more: In some cases of delegates this style is useful for something different. Imagine a click handler consisting of a function func somethingGotClicked(sender:YourSenderType). If your class creates multiple instances of YourSenderType and shows them at the same time registering itself as their delegate there would be no way to know which one got clicked if there was no sender parameter. In some func somethingGotClicked() you would not know which one got clicked. That capability is often needed when showing multiple UITableView or UICollectionView is one single view with one single instances set as their delegate.