Search code examples
iosswiftuibuttonaddtarget

EXC_BAD_ACCESS, When Calling Button Method From Another Class


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


Solution

  • 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)
    }