Search code examples
swiftsubclassdrawuibezierpathcgrect

missing argument for parameter in coding error in trying to subclass a uibezierPath


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

Solution

  • 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:

    1. 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).

    2. 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?

    3. 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.

    4. 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.