I've got an NSButton in a View Controller that, when clicked, should call a method in an instance of another class (I have that instance in the View Controller). However, the action method is never called.
My code is below (it's short and simple). Please can somebody explain why this is?
View Controller class with the button:
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let b:NSButton = NSButton(frame: NSRect(x: 150, y: 200, width: 30, height: 30))
self.view.addSubview(b)
let g = Global()
b.target = g
b.action = #selector(g.s)
}
}
Class called 'Global' that I create an instance of, that the button should then call a method within:
class Global:NSObject {
override init() {
super.init()
}
#objc dynamic func s() {
Swift.print("S ran")
}
}
Thanks
Update: For easy reproduction, I've created a GitHub repo showing the issue in its simplest form here.
The problem is that by the time you click the button, target has been set to nil. This is because g is stored as a local variable and target is a weak property, so after viewDidLoad is finished, g is released and the target becomes nil. So, by the time you click the button, there is no object on which to call the action.
You need to store a strong reference to the target somewhere. One way would be to store it as an instance variable on your view controller:
class ViewController: NSViewController {
let g = Global()
override func viewDidLoad() {
super.viewDidLoad()
let b:NSButton = NSButton(frame: NSRect(x: 150, y: 200, width: 30, height: 30))
self.view.addSubview(b)
b.target = g
b.action = #selector(g.s)
}
}
Related
I'm creating a combo box programmatically and I would like to cause the selection from the drop down menu to call a function.
I can do it if i'm creating a button using - myButton.action = #selector(some_function(_:)), but it doesn't work with an NSComboBox.
here is an example from my code:
func populate_scroller_with_combobox(json_file: Array<Any>, panel: NSView)
{
let combox = NSComboBox()
combox.identifier = NSUserInterfaceItemIdentifier(rawValue: "combobox1")
combox.addItem(withObjectValue: "None")
combox.addItems(withObjectValues: json_file_content)
combox.numberOfVisibleItems = 10
combox.isEditable = false
combox.action = #selector(some_function(_:))
combox.selectItem(withObjectValue: "None")
panel.addSubview(combox)
combox.frame = CGRect(x:190, y: 30, width: 170, height: 26)
}
#objc func some_function(_ sender: NSButton)
{
print ( "Combobox value changed." )
}
It's been a looooonnnnggg time since I've used an NSComboBox.
Reviewing the docs, it seems to follow the data source & delegate pattern that table views use.
You need to have some object conform to the NSComboBoxDelegate protocol and set it as the combo box's delegate. Then your delegate's comboBoxSelectionDidChange(_:) method will be called when the user makes a selection.
You could create a custom NSControl that manages an NSComboBox and serves as the combo box's delegate, and have the control generate an IBAction with an event type of valueChanged when the user selects an item.
here is the solution in case that anyone else run into the same issue:
add to your ViewController Class 'NSComboBoxDelegate'.
to your combobox creation function add 'combox.delegate = self'.
add another function called func 'comboBoxSelectionDidChange(_ notification: Notification)' which will be triggered by the selection changes
class MainViewController: NSViewController, NSComboBoxDelegate
{
override func viewDidLoad()
{
super.viewDidLoad()
}
func populate_scroller_with_combobox(json_file: Array<Any>, panel: NSView)
{
let combox = NSComboBox()
combox.identifier = NSUserInterfaceItemIdentifier(rawValue: "combobox1")
combox.addItem(withObjectValue: "None")
combox.addItems(withObjectValues: json_file_content)
combox.numberOfVisibleItems = 10
combox.isEditable = false
combox.selectItem(withObjectValue: "None")
combox.delegate = self
panel.addSubview(combox)
combox.frame = CGRect(x:190, y: 30, width: 170, height: 26)
}
func comboBoxSelectionDidChange(_ notification: Notification)
{
print("Combobox value changed.")
}
}
Here is the situation. I have a protocol, and extension of it.
protocol CustomViewAddable {
var aView: UIView { get }
var bView: UIView { get }
func setupCustomView()
}
extension CustomViewAddable where Self: UIViewController {
var aView: UIView {
let _aView = UIView()
_aView.frame = self.view.bounds
_aView.backgroundColor = .grey
// this is for me to observe how many times this aView init.
print("aView: \(_aView)")
return _aView
}
var bView: UIView {
let _bView = UIView(frame: CGRect(x: 30, y: 30, width: 30, height: 30))
_bView.backgroundColor = .yellow
return _bView
}
func setupCustomView() {
view.addSubview(aView);
aView.addSubview(bView);
}
}
And I make a ViewController to conform this protocol then I add this custom 'aView' to my ViewController's view.
class MyVC: UIViewController, CustomViewAddable {
override func viewDidLoad() {
super.viewDidLoad()
setupCustomView()
}
}
I run it. In my console log it prints twice of init and I trying to do something in my custom 'aView' and it failed. (The code I paste above that I simplified so that it'll be very easy to show my intension)
Could anybody to explain why or make a fix to it that I'll be very appreciated.
Because your var aView: UIView is computed variable not store variable,
So every time you call aView, it will create a new UIView,
You can use Associated Objects in NSObject here is some tutorials:
swift-objc-runtime
associated-objects
Hope this may help.
Basically in the way you implemented the setupCustomView method nothing should work because as mentioned in another response you're using a computed property, so this implies that every time you access the property it's created again.
You don't need to use associated-objects or something like that to achieve what you want, you only need to keep the reference of the aView at the beginning avoiding calling it again, in this way:
func setupCustomView() {
let tView = aView // only is computed once
view.addSubview(tView)
tView.addSubview(bView)
}
I hope this help you.
I am organizing a code an app that iPad Air 2 I use to control finances of a company (as a developer without AppStore). This app displays painels with information, and these panels are displayed and hidden as the user action.
I decided to try to create different ways and separate my schedule between arquivos.swift.
That's my ViewController. Only two buttons, it's all I do for Interface Builder (for example). The rest of the application is created programmatically.
This ViewController is connected directly to the ViewController class:
class ViewController: UIViewController {
override func prefersStatusBarHidden() -> Bool {
return true
}
var panelInfo: PanelInfo!
var selfSize: CGSize {
get {
return self.view.frame.size
}
}
override func viewDidLoad() {
super.viewDidLoad()
panelInfo = PanelInfo(viewControllerSize: selfSize)
self.view.addSubview(panelInfo.panel)
}
#IBAction func showPropNavigationbar(sender: AnyObject) {
panelInfo.show()
}
#IBAction func hidePropNavigationbar(sender: AnyObject) {
panelInfo.hide()
}
}
To create a view that will be displayed where the information, I created a class that manages it.
class PanelInfo {
/*** View Controller Size ***/
private var viewSize: CGSize!
/*** View ***/
internal var panel: UIView!
/*** View Panel Size ***/
private var size: CGSize!
/*** View Position ***/
private var originShow: CGPoint!
private var originHide: CGPoint!
private var closeButton: UIButton!
/*** Start the instance ***/
init(viewControllerSize: CGSize) {
/*** Set values in variables above ***/
viewSize = viewControllerSize
size = CGSize(width: 300, height: viewSize.height)
originShow = CGPoint(x: viewSize.width - size.width, y: 0)
originHide = CGPoint(x: viewSize.width, y: 0)
/*** Create the panel view ***/
panel = UIView(frame: CGRect(origin: originHide, size: self.size))
panel.backgroundColor = UIColor(red: 0.014, green: 0.106, blue: 0.179, alpha: 1.000)
/*** Create the closeButton ***/
closeButton = UIButton(type: .System)
closeButton.frame = CGRect(origin: CGPoint(x: 8, y: 8), size: CGSize(width: 32, height: 32))
closeButton.setImage(UIImage(named: "closeButton"), forState: .Normal)
panel.addSubview(closeButton)
}
func hidepanel(sender: AnyObject) {
print(sender)
self.hide()
}
/*** Show panel in ViewController ***/
internal func show() -> Void {
UIView.animateWithDuration(0.3) { () -> Void in
self.panel.frame.origin = self.originShow
}
}
/*** Hide panel in ViewController ***/
internal func hide() -> Void {
UIView.animateWithDuration(0.3) { () -> Void in
self.panel.frame.origin = self.originHide
}
}
}
When the code is executed, and I touch in 'Show', the panel appears. Working perfectly.
When I touch in 'Hide', the panel disappears. Working perfectly.
The Problem
I'm trying to create a button for programming and add a target to run make the panel close. This button is closeButton.
If i added the target (nil, action: "hidepanel:", forControlEvents: .TouchUpInside
Nothing happens
If I change for closeButton.addTarget(nil, action: "hidepanel:", forControlEvents: .TouchUpInside
Nothing happens
If I change for closeButton.addTarget(self, action: "hidepanel:", forControlEvents: .TouchUpInside)
The error appear:
*** NSForwarding: warning: object 0x7a99ee00 of class 'Panel_Model.PanelInfo' does not implement methodSignatureForSelector: -- trouble ahead
Unrecognized selector -[Panel_Model.PanelInfo hide panel:]
If I try change for closeButton.addTarget(self, action: "hidepanel", forControlEvents: .TouchUpInside)
The previous error appears
Note: This is a example. The app original is bigger. When I started to build the app, had little time to do it and had little experience in Swift (and Obj-C as well), so I created everything needed in a ViewController. Today I am with almost 5000 lines of programming, and need to find a better way to organize.
I believe that the error can be related to (target: AnyObject?, ...) , the first item of AddTarget, for the reason of being used in a class without superclass... But I can't find a way to solve it.
My question:
1º This is the best form of organization?
2º Knowing that I am using a class, and this class have a variabel controlling a UIView. Can I implement added buttons to view and set targets on the buttons?
3º If possible, what is the best way to do this, could show examples?
Try the following
let button:UIButton = UIButton(frame: CGRect(x: 0, y: 0, width: 70, height: 30))
button.addTarget(self, action: Selector("hidepanel:"), forControlEvents: UIControlEvents.TouchUpInside)
Update
Try inheriting PanelInfo from `NSObject.
Below is just a test of delegation.
What I did was, 1) draw a rectangle, 2) set this rectangle's width of line with a delegate, 3) Hope the storyboard could update its display.
There are two questions:
The first is: If I use "testView.widthdelegate = ViewController()" rather than "testView.widthdelegate = self" , the "var widthValue: CGFloat? = widthdelegate?.trueWidth" will be nil, but it should be 50, what's different between "self" and "ViewController()"?
The second is: I still want to update the result of draw in storyboard, where you can see I did a SetNeedDisplay() but no use at all, how could I do it?
View
import UIKit
protocol widthDelegate: class {
var trueWidth: CGFloat { get }
}
#IBDesignable
class TestView: UIView {
weak var widthdelegate: widthDelegate?
override func drawRect(rect: CGRect) {
var widthValue: CGFloat? = widthdelegate?.trueWidth ?? 1.0
rectangle(widthRefer: widthValue!)
println("width in TestView is \(widthdelegate?.trueWidth)" )
}
func rectangle(#widthRefer: CGFloat) -> UIBezierPath{
var rect = UIBezierPath(rect: CGRect(x: bounds.width/2-50, y: bounds.height/2-50, width: 100, height: 100))
rect.lineWidth = widthRefer
rect.stroke()
return rect
}
}
Controller
import UIKit
class ViewController: UIViewController,widthDelegate {
var trueWidth: CGFloat = 50
#IBOutlet var testView: TestView!{
didSet{ //after the storyboard loaded.
// testView.widthdelegate = ViewController()
testView.widthdelegate = self
testView.setNeedsDisplay()
}
}
}
Answer 1:
self is the actual instance of the class the code is in (correct solution)
ViewController() creates a brand new instance of ViewController which is not identical with the instance created in IB (wrong solution)
Answer 2:
Never implement didSet for an IBOutlet because it's never called during initialization. Better use viewDidLoad() for settings
Some other notes:
Please consider the naming convention that class, protocol and enum names start with a capital letter.
The class constraint in the protocol declaration is not needed
I have a very general question about the Initialization in Swift.
Unlike in Objective C it's now possible to call the init() directly at the declaration outside of my functions:
e.g.
class ViewController: UIViewController {
let myView: UIView = UIView()
override func viewDidLoad() {
super.viewDidLoad()
myView.frame = getFrame()
myView.backgroundColor = UIColor.redColor()
self.view.addSubview(myView)
}
func getFrame() -> CGRect {
return CGRectMake(0, 0, 100, 100)
}
}
In Objective C I would have done the initialization in my function.
But what if I want to call an Initializer with parameters which are not set yet?
e.g.
I want to init with a frame which is being calculated in a func()
class ViewController: UIViewController {
//THIS IS NOT WOKRING
let myView: UIView = UIView(frame: getFrame())
override func viewDidLoad() {
super.viewDidLoad()
myView.backgroundColor = UIColor.redColor()
self.view.addSubview(myView)
}
func getFrame() -> CGRect {
return CGRectMake(0, 0, 100, 100)
}
}
I don't wanna do my initializations at two different places in the Code. Is there any general pattern for the initializations?
So your options for initialisation in swift are numerous. With your current example you cannot use the method getFrame() yet because you do not yet have a reference to self as the ViewController has not get been initialised. Instead you could use:
let myView: UIView = UIView(frame: CGRectMake(0, 0, 100, 100))
As this does not require the reference to self. Alternatively you could lazy instantiation which will get run after self is available (this can only be used with var not let:
lazy var myView: UIView = {
return UIView(frame:self.getFrame())
}()
To answer your question, when using UIKit class where you often don't have control over their instantiation you can keep doing it the same was as you were in objective c and use implicitly unwrapped optionals (to prevent you having to use a ! or ? every time you instantiate a variable, e.g.:
var myView: UIView!
override func viewDidLoad(){
super.viewDidLoad();
myView = UIView(frame:getFrame())
}
This however does not work with let constants, as they need to be assigned either immediately or in the constructor of the object. Hope this helps.