I want my swift code to display a uibezierPath button. The code uses override func draw to draw the button. The code is getting a compile error. Its telling me I am missing a parameter in let customButton = FunkyButton(coder: <#NSCoder#>) you can see the error in NSCODER. I dont know what to put for nscoder. What do you think I should put?
import UIKit
class ViewController: UIViewController {
var box = UIImageView()
override open var shouldAutorotate: Bool {
return false
}
// Specify the orientation.
override open var supportedInterfaceOrientations: UIInterfaceOrientationMask {
return .landscapeRight
}
let customButton = FunkyButton(coder: <#NSCoder#>)
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(box)
// box.frame = CGRect(x: view.frame.width * 0.2, y: view.frame.height * 0.2, width: view.frame.width * 0.2, height: view.frame.height * 0.2)
box.backgroundColor = .systemTeal
customButton!.backgroundColor = .systemPink
self.view.addSubview(customButton!)
customButton?.addTarget(self, action: #selector(press), for: .touchDown)
}
@objc func press(){
print("hit")
}
}
class FunkyButton: UIButton {
var shapeLayer = CAShapeLayer()
let aPath = UIBezierPath()
override func draw(_ rect: CGRect) {
let aPath = UIBezierPath()
aPath.move(to: CGPoint(x: rect.width * 0.2, y: rect.height * 0.8))
aPath.addLine(to: CGPoint(x: rect.width * 0.4, y: rect.height * 0.2))
//design path in layer
shapeLayer.path = aPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.path = aPath.cgPath
// draw is called multiple times so you need to remove the old layer before adding the new one
shapeLayer.removeFromSuperlayer()
layer.addSublayer(shapeLayer)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.isHidden == true || self.alpha < 0.1 || self.isUserInteractionEnabled == false {
return nil
}
if aPath.contains(point) {
return self
}
return nil
}
}
When instantiating FunkyButton
, don’t manually call the coder
rendition. Just call
let button = FunkyButton()
Or add it in IB and hook up an outlet to
@IBOutlet weak var button: FunkyButton!
In FunkyButton
, you shouldn't update shape layer path inside draw(_:)
method. During initialization, just add the shape layer to the layer hierarchy, and whenever you update the shape layer’s path, it will be rendered for you. No draw(_:)
is needed/desired:
@IBDesignable
class FunkyButton: UIButton {
private let shapeLayer = CAShapeLayer()
private var path = UIBezierPath()
// called if button is instantiated programmatically (or as a designable)
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
// called if button is instantiated via IB
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
// called when the button’s frame is set
override func layoutSubviews() {
super.layoutSubviews()
updatePath()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
guard path.contains(point) else {
return nil
}
return super.hitTest(point, with: event)
}
}
private extension FunkyButton {
func configure() {
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 1
layer.addSublayer(shapeLayer)
}
func updatePath() {
path = UIBezierPath()
path.move(to: CGPoint(x: bounds.width * 0.2, y: bounds.height * 0.8))
path.addLine(to: CGPoint(x: bounds.width * 0.4, y: bounds.height * 0.2))
path.addLine(to: CGPoint(x: bounds.width * 0.2, y: bounds.height * 0.2))
path.close()
shapeLayer.path = path.cgPath
}
}
If you really want to draw your path in draw(_:)
, that is an acceptable pattern, too, but you wouldn't use CAShapeLayer
at all, and just manually stroke()
the UIBezierPath
in draw(_:)
. (If you implement this draw(_:)
method, though, do not use the rect
parameter of this method, but rather always refer back to the view’s bounds
.)
Bottom line, either use draw(_:)
(triggered by calling setNeedsDisplay
) or use CAShapeLayer
(and just update its path), but don't do both.
A few unrelated observations related to my code snippet:
You do not need to check for !isHidden
or isUserInteractionEnabled
in hitTest
, as this method won't be called if the button is hidden or has user interaction disabled. As the documentation says:
This method ignores view objects that are hidden, that have disabled user interactions, or have an alpha level less than 0.01.
I have also removed the alpha
check in hitTest
, as that is non-standard behavior. It is not a big deal, but this is the sort of thing that bites you later on (e.g. change button base class and now it behaves differently).
You might as well make it @IBDesignable
so that you can see it in Interface Builder (IB). There is no harm if you're only using it programmatically, but why not make it capable of being rendered in IB, too?
I have moved the configuration of the path into layoutSubviews
. Anything based upon the bounds
of the view should be responsive to changes in the layout. Sure, in your example, you are manually setting the frame
, but this is an unnecessary limitation to place on this button class. You might use auto-layout in the future, and using layoutSubviews
ensures that it will continue to function as intended. Plus, this way, the path will be updated if the size of the button changes.
There's no point in checking for contains
if the path is a line. So, I've added a third point so that I can test whether the hit point falls within the path.