0

I'm calling a method from my DrawMenuclass in my ViewControllerclass, which draws an oval (currently circle) button, pretty simple. It draws the button perfectly, but if I tap the button it crashes.

This happens even though I have created an instance of the ViewControllerclass in DrawMenu, and have used it for the 'target' parameter in 'button.addTarget'

Here is the code:

Button Method defined in DrawMenuclass:

func drawButton (superImageView: UIImageView, x_of_origin: CGFloat, y_of_origin: CGFloat, width_of_oval: CGFloat, height_of_oval: CGFloat, actionSelector: Selector, want_to_test_bounds:Bool) {

    var VC = ViewController()

    var button = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
    button.addTarget(VC, action: actionSelector, forControlEvents: UIControlEvents.TouchUpInside) 
    button.frame = CGRect(x: x_of_origin, y: y_of_origin, width: width_of_oval, height: height_of_oval)

    button.clipsToBounds = true
    button.layer.cornerRadius = height_of_oval/2.0 

    if (want_to_test_bounds == true) {
        button.layer.borderColor = UIColor.blackColor().CGColor
        button.layer.borderWidth = 1.0
        superImageView.userInteractionEnabled = true
        superImageView.addSubview(button)

    } else {
        superImageView.userInteractionEnabled = true
        superImageView.addSubview(button)
    }

}

Method called in ViewControllerclass:

override func viewDidLoad() {
    super.viewDidLoad()

    var drawMenu = DrawMenu()

    drawMenu.drawButton(imageView, x_of_origin: 100, y_of_origin: 150, width_of_oval: 100, height_of_oval: 100, actionSelector: "buttonTap:" as Selector, want_to_test_bounds: true) 
}

buttonTap also in ViewController class:

    func buttonTap(sender:UIButton!){
    println("Button is working")
}

Any help is appreciated. Thank You

  • What is the crash/exception message? – Paulw11 Sep 11 '15 at 04:42
  • Your drawButton sets a new instance of your ViewController as the target. You need to pass the existing ViewController as a parameter to drawButton so that it can be the target – Paulw11 Sep 11 '15 at 04:44
  • It crashes in the App Delegate with `Thead 1: EXC_BAD_ACCESS (code=EXC_1386_GPFLT).` – user2176152 Sep 11 '15 at 05:05
  • I've tried adding `var VC = ViewController() ` to the `ViewController` and passed it into the call. Also edited `Draw Button` to `func drawButton (superImageView: UIImageView, class_where_method_is_called: AnyObject ,x_of_origin: CGFloat, y_of_origin: CGFloat, width_of_oval: CGFloat, height_of_oval: CGFloat, actionSelector: Selector,want_to_test_bounds:Bool) { var button = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton button.addTarget(class_where_method_is_called, action: actionSelector, forControlEvents: UIControlEvents.TouchUpInside)` But same error – user2176152 Sep 11 '15 at 05:09
  • You don't want to create a new VC at all - you simply use `self` to refer to the existing object – Paulw11 Sep 11 '15 at 05:24

2 Answers2

2

In the method drawButton you are setting the target for the touchUpInside to a new instance of the view controller. This reference is created in a local variable in drawButton and will be released when that method exits. When the action handler is triggered it attempts to call the function on the invalid object and you get a crash.

The correct design pattern to use here is a delegate - Define a protocol for the handler and have your view controller implement that protocol. You can then pass the view controller to the drawButton method -

Start by defining the protocol in DrawMenu -

protocol ButtonDelegate:NSObjectProtocol
{
    func buttonTap(sender: UIButton!) -> Void
}

Then you can use the protocol reference in your drawButton method -

func drawButton (superImageView: UIImageView, x_of_origin: CGFloat, y_of_origin: CGFloat, width_of_oval: CGFloat, height_of_oval: CGFloat, delegate: ButtonDelegate, want_to_test_bounds:Bool) {

    var button = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
    button.addTarget(delegate, action: "buttonTap:", forControlEvents: UIControlEvents.TouchUpInside) 
    button.frame = CGRect(x: x_of_origin, y: y_of_origin, width: width_of_oval, height: height_of_oval)

    button.clipsToBounds = true
    button.layer.cornerRadius = height_of_oval/2.0 

    if (want_to_test_bounds == true) {
        button.layer.borderColor = UIColor.blackColor().CGColor
        button.layer.borderWidth = 1.0
        superImageView.userInteractionEnabled = true
        superImageView.addSubview(button)

    } else {
        superImageView.userInteractionEnabled = true
        superImageView.addSubview(button)
    }

}

Finally, make sure your ViewController implements the protocol -

class ViewController : UIViewController, ButtonDelegate

And pass the reference to the ViewController instance when you create the button -

override func viewDidLoad() {
    super.viewDidLoad()

    var drawMenu = DrawMenu()

    drawMenu.drawButton(imageView, x_of_origin: 100, y_of_origin: 150, width_of_oval: 100, height_of_oval: 100, delegate: self, want_to_test_bounds: true) 
}

Other improvements I would suggest is making the drawButton method a static, class method so you don't need to instantiate an instance of DrawMenu to use it and having the method simply return the button rather than passing the method a view to add the button to. The way you have it, it is difficult to get a reference to the button if you want to make further changes. Changing the function in this way also makes it simple to add the button to views that aren't UIImageViews

Finally, use a CGRect rather than passing distinct x,y,width,height

class func drawButton (frame: CGRect, delegate: ButtonDelegate, showOutline:Bool) -> UIButton {

    var button = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
    button.addTarget(delegate, action: "buttonTap:", forControlEvents: UIControlEvents.TouchUpInside) 
    button.frame = frame
    button.clipsToBounds = true
    button.layer.cornerRadius = frame.size.height/2.0 

    if (showOutline) {
        button.layer.borderColor = UIColor.blackColor().CGColor
        button.layer.borderWidth = 1.0
    }
    return button
}

Then you would say -

override func viewDidLoad() {
    super.viewDidLoad()

    var newButton = DrawMenu.drawButton(CGRect(x: 100, y: 150, width: 100, height: 100), 
      delegate: self, 
      showOutline: true) 
    imageView.userInteractionEnabled = true
    imageView.addSubview(newButton)
}
Paulw11
  • 108,386
  • 14
  • 159
  • 186
  • That makes sense, thank you for putting in so much effort. Very well explained, and thanks for the improvements. I keep getting the error `Cannot invoke 'addTarget' with an argument list of type '(ButtonDelegate, action: Selector, forControlEvents: UIControlEvents)'` with `button.addTarget(delegate, action: actionSelector, forControlEvents: UIControlEvents.TouchUpInside` Am I missing something? – user2176152 Sep 11 '15 at 05:55
  • 1
    `NSObjectProtocol` was missing from the protocol declaration – Paulw11 Sep 11 '15 at 06:08
0

When you add a target to a button it is important to set a right target object. in your case you have to pass the View Controller object to the drawButton method and you have to use that ViewController object when you add a target to button.

Because when a button event called it finds the selector in that exact target! So in your case when you instantiate a ViewController object inside the method, the object is deallocated when the method call is end.

func drawButton (superImageView: UIImageView, inViewController: UIViewController, x_of_origin: CGFloat, y_of_origin: CGFloat, width_of_oval: CGFloat, height_of_oval: CGFloat, actionSelector: Selector, want_to_test_bounds:Bool)
{
    var button = UIButton.buttonWithType(UIButtonType.Custom) as! UIButton
    button.addTarget(inViewController, action: actionSelector, forControlEvents: UIControlEvents.TouchUpInside)
    button.frame = CGRect(x: x_of_origin, y: y_of_origin, width: width_of_oval, height: height_of_oval)

    button.clipsToBounds = true
    button.layer.cornerRadius = height_of_oval/2.0

    if (want_to_test_bounds == true)
    {
        button.layer.borderColor = UIColor.blackColor().CGColor
        button.layer.borderWidth = 1.0
        superImageView.userInteractionEnabled = true
        superImageView.addSubview(button)

    }
    else
    {
        superImageView.userInteractionEnabled = true
        superImageView.addSubview(button)
    }
}
Jason Nam
  • 2,011
  • 2
  • 13
  • 22