Reputation: 49
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
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
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
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