Blake Byers
Blake Byers

Reputation: 49

How can I access a method that I defined in a different file?

Here I try to access my handleTap() function which is an OBJ C function I created in a different file, I need it to stay in that file so how can I access that function from my SecondViewController?

import UIKit

class SecondViewController: UIViewController {
    
   
    var stackView = UIStackView()
    
    var circle: Button = {
        let button = Button()
        button.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
        return button
    }()

This is how I defined handleTap() in another file near the bottom. It is a file name Button.swift, it may be possible to make an extension on the SecondViewController to include handleTap(), but I don't know how.

    import UIKit

class Button: UIButton {
    override init(frame: CGRect) {
        super.init(frame: frame)
        createCircle()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    let percentageLabel: UILabel = {
        let label = UILabel()
        label.text = ""
        label.textAlignment = .center
        label.font = UIFont.boldSystemFont(ofSize: 28)
        label.textColor = UIColor(red: 0.59, green: 0.42, blue: 0.23, alpha: 1.00)
        return label
        
    }()
    let shapeLayer = CAShapeLayer()
    
    func createCircle() {
        
        let trackLayer = CAShapeLayer()
        
        let button = UIButton(type: .custom)
        button.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        button.layer.cornerRadius = 0.5 * button.bounds.size.width
        button.clipsToBounds = true
        button.center = center
        
        addSubview(button)
        
        let circularPath = UIBezierPath(arcCenter: .zero, radius: 50, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
        
        trackLayer.path = circularPath.cgPath
        
        trackLayer.strokeColor = UIColor(red: 0.82, green: 0.69, blue: 0.52, alpha: 1.00).cgColor
        trackLayer.fillColor = UIColor.clear.cgColor
        trackLayer.lineWidth = 10
        trackLayer.position = center
        layer.addSublayer(trackLayer)
        
        shapeLayer.path = circularPath.cgPath
        
        shapeLayer.strokeColor = UIColor(red: 0.59, green: 0.42, blue: 0.23, alpha: 1.00).cgColor
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.lineWidth = 10
        shapeLayer.lineCap = CAShapeLayerLineCap.round
        shapeLayer.position = center
        shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2, 0, 0, 1)
        
        addSubview(percentageLabel)
        
        percentageLabel.frame = CGRect(x: 0, y: 0, width: 150, height: 150)
        percentageLabel.center = center
        //  return CGPoint(x: positionX, y: positionY)
        
    }
    
    var done = 0
    var toDo = 0
    
    @objc func handleTap(sender: UIButton) {
        toDo = 5
        if done < toDo {
            done += 1
        } else {
            done -= toDo
        }
        let percentage = CGFloat(done) / CGFloat(toDo)
        percentageLabel.text = "\(Int(percentage * 100))%"
        
        DispatchQueue.main.async {
            self.shapeLayer.strokeEnd = percentage
        }
        layer.addSublayer(shapeLayer)
    }
}

Upvotes: 0

Views: 48

Answers (3)

DonMag
DonMag

Reputation: 77477

You are doing a number of things incorrectly...

First, you are creating a subclass of UIButton, but instead of using the button and its built-in label, your code is adding another button as a subview, and adding another label as a subview.

Then you are trying to add a target action in your controller... but your tap handler is inside your custom button.

Here is a way to NOT add another button-as-subview, and to use the built-in title label, and to add the target action to call the "internal" handleTap() func:

class Button: UIButton {

    override init(frame: CGRect) {
        super.init(frame: frame)
        createCircle()
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // use the built-in button label
    // do NOT add another label as a subview!!!
    //let percentageLabel: UILabel = {
    //  let label = UILabel()
    //  label.text = ""
    //  label.textAlignment = .center
    //  label.font = UIFont.boldSystemFont(ofSize: 28)
    //  label.textColor = UIColor(red: 0.59, green: 0.42, blue: 0.23, alpha: 1.00)
    //  return label
    //}()

    // shape layers
    let trackLayer = CAShapeLayer()
    let shapeLayer = CAShapeLayer()
    
    func createCircle() {

        // do NOT add another button as a subview!!!!
        //let button = UIButton(type: .custom)
        //button.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        //button.layer.cornerRadius = 0.5 * button.bounds.size.width
        //button.clipsToBounds = true
        //button.center = center

        //addSubview(button)
        
        // add the shape layers as sublayers
        layer.addSublayer(trackLayer)
        layer.addSublayer(shapeLayer)

        // non-changing trackLayer properties
        trackLayer.strokeColor = UIColor(red: 0.82, green: 0.69, blue: 0.52, alpha: 1.00).cgColor
        trackLayer.fillColor = UIColor.clear.cgColor
        trackLayer.lineWidth = 10
        
        // changing shapeLayer properties
        shapeLayer.strokeColor = UIColor(red: 0.59, green: 0.42, blue: 0.23, alpha: 1.00).cgColor
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.lineWidth = 10
        shapeLayer.lineCap = CAShapeLayerLineCap.round
        shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2, 0, 0, 1)

        // start at Zero percent
        shapeLayer.strokeEnd = 0
        
        // use the UIButton built-in label
        titleLabel?.font = UIFont.boldSystemFont(ofSize: 28)
        
        // normal title color
        setTitleColor(UIColor(red: 0.59, green: 0.42, blue: 0.23, alpha: 1.00), for: .normal)
        // highlighted title color (just making it darker)
        setTitleColor(UIColor(red: 0.59 * 0.3, green: 0.42 * 0.3, blue: 0.23 * 0.3, alpha: 1.00), for: .highlighted)

        // start at Zero percent
        setTitle("0%", for: [])
        
        // we want a tap to call our Internal handleTap() function
        addTarget(self, action: #selector(handleTap(sender:)), for: .touchUpInside)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()

        // update layer frames
        trackLayer.frame = bounds
        shapeLayer.frame = bounds

        // set the shape layer paths
        // shape strokes are centered on the path,
        //  so make the path 1/2 the lineWidth smaller
        let r = bounds.insetBy(dx: trackLayer.lineWidth * 0.5, dy: trackLayer.lineWidth * 0.5)
        let circularPath = UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: r.width * 0.5, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)

        trackLayer.path = circularPath.cgPath
        shapeLayer.path = circularPath.cgPath
    }
    
    var done = 0
    var toDo = 0
    
    @objc func handleTap(sender: UIButton) {
        toDo = 5
        if done < toDo {
            done += 1
        } else {
            done -= toDo
        }
        
        let percentage = CGFloat(done) / CGFloat(toDo)
        
        // update title
        self.setTitle("\(Int(percentage * 100))%", for: [])
        
        // update strokeEnd of the shape layer
        self.shapeLayer.strokeEnd = percentage
    }
    
}

and a view controller to see it:

class CircleButtonViewController: UIViewController {

    let stackView = UIStackView()
    
    let circle: Button = Button()

    override func viewDidLoad() {
        super.viewDidLoad()
        
        stackView.axis = .vertical
        stackView.spacing = 20
        stackView.alignment = .center
        
        stackView.translatesAutoresizingMaskIntoConstraints = false

        // an "info label"
        let labelA = UILabel()
        labelA.numberOfLines = 0
        labelA.textAlignment = .center
        labelA.text = "Internal\nTap is handled inside the button itself."
        
        // add label and the button to the stackView
        stackView.addArrangedSubview(labelA)
        stackView.addArrangedSubview(circle)

        // add the stack view to the view
        view.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            
            // center the stackView in the view
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),

            // make it 80% the width of the view
            stackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
            
            // make the circle button 100 x 100 pts
            circle.widthAnchor.constraint(equalToConstant: 100.0),
            circle.heightAnchor.constraint(equalTo: circle.widthAnchor),
            
        ])
        
    }
    
}

Handling the tap inside the button class is probably NOT what you want to do though. More likely, you want your controller to handle the tap, and update the percentage of your custom button.

So, here's a button class to do that - I called it ButtonV2:

class ButtonV2: UIButton {
    
    var percentage: CGFloat = 0 {
        didSet {
            updateShape()
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        createCircle()
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // shape layers
    let trackLayer = CAShapeLayer()
    let shapeLayer = CAShapeLayer()
    
    func createCircle() {
        
        // add the shape layers as sublayers
        layer.addSublayer(trackLayer)
        layer.addSublayer(shapeLayer)
        
        // non-changing trackLayer properties
        trackLayer.strokeColor = UIColor(red: 0.82, green: 0.69, blue: 0.52, alpha: 1.00).cgColor
        trackLayer.fillColor = UIColor.clear.cgColor
        trackLayer.lineWidth = 10
        
        // changing shapeLayer properties
        shapeLayer.strokeColor = UIColor(red: 0.59, green: 0.42, blue: 0.23, alpha: 1.00).cgColor
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.lineWidth = 10
        shapeLayer.lineCap = CAShapeLayerLineCap.round
        shapeLayer.transform = CATransform3DMakeRotation(-CGFloat.pi / 2, 0, 0, 1)
        
        // start at Zero percent
        shapeLayer.strokeEnd = 0
        
        // use the UIButton built-in label
        titleLabel?.font = UIFont.boldSystemFont(ofSize: 28)
        
        // normal title color
        setTitleColor(UIColor(red: 0.59, green: 0.42, blue: 0.23, alpha: 1.00), for: .normal)
        // highlighted title color (just making it darker)
        setTitleColor(UIColor(red: 0.59 * 0.3, green: 0.42 * 0.3, blue: 0.23 * 0.3, alpha: 1.00), for: .highlighted)
        
        // start at Zero percent
        setTitle("0%", for: [])
        
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        // update layer frames
        trackLayer.frame = bounds
        shapeLayer.frame = bounds
        
        // set the shape layer paths
        // shape strokes are centered on the path,
        //  so make the path 1/2 the lineWidth smaller
        let r = bounds.insetBy(dx: trackLayer.lineWidth * 0.5, dy: trackLayer.lineWidth * 0.5)
        let circularPath = UIBezierPath(arcCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: r.width * 0.5, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)

        trackLayer.path = circularPath.cgPath
        shapeLayer.path = circularPath.cgPath
    }
    
    func updateShape() {
        // update title
        self.setTitle("\(Int(percentage * 100))%", for: [])
        
        // update strokeEnd of the shape layer
        self.shapeLayer.strokeEnd = percentage
    }
    
}

and here's a controller class to show the difference:

class V2CircleButtonViewController: UIViewController {
    
    let stackView = UIStackView()
    
    let circle: Button = Button()
    
    let circleV2: ButtonV2 = ButtonV2()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        stackView.axis = .vertical
        stackView.alignment = .center
        stackView.spacing = 20
        
        stackView.translatesAutoresizingMaskIntoConstraints = false
        
        // a couple "info labels"
        let labelA = UILabel()
        labelA.numberOfLines = 0
        labelA.textAlignment = .center
        labelA.text = "Internal\nTap is handled inside the button itself."
        
        let labelB = UILabel()
        labelB.numberOfLines = 0
        labelB.textAlignment = .center
        labelB.text = "External\nTap is handled by the controller, which sets the .percentage property of the button."

        // add labels and the buttons to the stackView
        stackView.addArrangedSubview(labelA)
        stackView.addArrangedSubview(circle)
        stackView.addArrangedSubview(labelB)
        stackView.addArrangedSubview(circleV2)

        // add the stack view to the view
        view.addSubview(stackView)
        
        NSLayoutConstraint.activate([
            
            // center the stackView in the view
            stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            
            // make it 80% the width of the view
            stackView.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
            
            // make the circle buttons 100 x 100 pts
            circle.widthAnchor.constraint(equalToConstant: 100.0),
            circle.heightAnchor.constraint(equalTo: circle.widthAnchor),
            
            circleV2.widthAnchor.constraint(equalToConstant: 100.0),
            circleV2.heightAnchor.constraint(equalTo: circleV2.widthAnchor),
            
        ])
        
        // add a target action for circleV2 button
        circleV2.addTarget(self, action: #selector(handleTap(sender:)), for: .touchUpInside)
        
    }
    
    var done = 0
    var toDo = 0
    
    @objc func handleTap(sender: UIButton) {
        toDo = 5
        if done < toDo {
            done += 1
        } else {
            done -= toDo
        }
        
        // update circleV2 button
        circleV2.percentage = CGFloat(done) / CGFloat(toDo)

        // do something else because the button was tapped?

    }

}

When you run this, you'll see that both custom buttons look and behave the same ... the big difference is that in V2 your controller is tracking the done / todo values, so you can set them as desired and also your controller can "do something else" in response to the button tap.

Upvotes: 1

ColdLogic
ColdLogic

Reputation: 7275

To solve your problem, you should move your addTarget call into your Button init function.

override init(frame: CGRect) {
    super.init(frame: frame)
    addTarget(self, action: #selector(handleTap), for: .touchUpInside)
    createCircle()
}

Back in SecondViewController, you then need to change your button declaration to:

var circle: Button = {
    let button = Button()
    return button
}()

or even more simply

var circle: Button = Button()

This is assuming that you are creating a very special subclass of button that always has its handleTap function called. Usually, we use concrete UIButtons (non-subclassed ones) to add a target that would point to a function that handles the event on the view controller. This would mean that handleTap would be on SecondViewController. But you stated you cant move that function.

Basically all the functionality you have in your Button class would be in SecondViewController. I cant quite tell what your button subclass is suppose to do, it looks like its all view logic, and shouldn't be on a button subclass.

Upvotes: 0

Victor Sebastian
Victor Sebastian

Reputation: 197

I think, in order to use the function of another class, the target must be set to that class and the function must be a static class method.

Refer this link: how-to-add-an-action-to-a-uibutton-that-is-in-another-class

Upvotes: 0

Related Questions