iosswiftcustom-button

Swift - Custom UIButton Title Not Showing


I wanted to create hexagonal buttons that will not overlap if they are displayed in "honeycomb" fashion and only the correct button will be able selected. I found subclass to mask UIView here and I used it for my UIButton class to do the masking. It works fine!

enter image description here

When I generate the buttons in the view, the button titles are not visible. Research showed that I need to be using super.layoutSubviews() to be able to display the text for the title. My print method shows that the title was populated correctly and it is corresponding with another object class, but no matter what, the title won't show. Expected title is "H" and the print log shows the title exist in the first button:

Getting category: Category(date: 2021-07-11 10:29:45 +0000, categoryID: "C1", categoryTitle: "Home", shortTitle: "H", categoryColor: UIExtendedSRGBColorSpace   0.403922 0.643137 0.921569 1, isDefault: false)
setupProperties short title: H
Short button title: H
Button Title: Optional("H")
Getting category: Category(date: 2021-07-11 10:15:50 +0000, categoryID: "C2", categoryTitle: "", shortTitle: "", categoryColor: UIExtendedSRGBColorSpace 0.960784 0.960784 0.960784 1, isDefault: true)
setupProperties short title: 
Short button title: 
Button Title: nil

I even tried to add another label to the UIButton subviews and bring this label to front in the same method, but it won't show either.

Is there another way to force the custom button to display the title text or do I have to create UILabel with the title in the same position as the button and place it in the layer above the button? This approach is not preferred...

Is it because of the UIButton is masked with polygon?

HEXA BUTTON CLASS:

class HexaButton: UIButton {

var path: UIBezierPath!
var category: Category!


init(cat: Category) {
    super.init(frame: CGRect())
    self.category = cat
    self.setupProperties()
    //self.layoutSubviews()
}

required init?(coder: NSCoder) {
    super.init(coder: coder)
    self.setupProperties()
    //self.layoutSubviews()
    fatalError("init(coder:) has not been implemented")
}

override func awakeFromNib() {
    addTarget(self, action: #selector(touchDown), for: .touchDown)
    self.setupProperties()
    //self.layoutSubviews()
}

private func setupProperties(){
    self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 10)
    self.setTitleColor(UIColor.white, for: .normal)
    self.setTitle(category.shortTitle, for: .normal)
    
    print("setupProperties short title: \(category.shortTitle)")

}

override func layoutSubviews() {
    super.layoutSubviews()
    self.titleLabel?.font = UIFont.boldSystemFont(ofSize: 10)
    self.setTitleColor(UIColor.white, for: .normal)
    self.setTitle(category.shortTitle, for: .normal)
}

override func draw(_ rect: CGRect) {
  
    let lineWidth: CGFloat = 5
    path = self.roundedPolygonPath(rect: self.bounds, lineWidth: lineWidth, sides: 6, cornerRadius: 10, rotationOffset: CGFloat(.pi / 2.0))

    let shapeLayer = CAShapeLayer()
    shapeLayer.strokeColor = category.categoryColor.cgColor
    shapeLayer.fillColor = category.categoryColor.cgColor
    shapeLayer.path = path.cgPath
    layer.addSublayer(shapeLayer)
}

@objc func touchDown(button: HexaButton, event: UIEvent) {
    if let touch = event.touches(for: button)?.first {
        let location = touch.location(in: button)

        if path.contains(location) == false {
            button.cancelTracking(with: nil)
        }
    }
}

public func roundedPolygonPath(rect: CGRect, lineWidth: CGFloat, sides: NSInteger, cornerRadius: CGFloat, rotationOffset: CGFloat = 0) -> UIBezierPath {
        let path = UIBezierPath()
        let theta: CGFloat = CGFloat(2.0 * .pi) / CGFloat(sides) // How much to turn at every corner
        let width = min(rect.size.width, rect.size.height)        // Width of the square
        
        let center = CGPoint(x: rect.origin.x + width / 2.0, y: rect.origin.y + width / 2.0)
        
        // Radius of the circle that encircles the polygon
        // Notice that the radius is adjusted for the corners, that way the largest outer
        // dimension of the resulting shape is always exactly the width - linewidth
        let radius = (width - lineWidth + cornerRadius - (cos(theta) * cornerRadius)) / 2.0
        
        // Start drawing at a point, which by default is at the right hand edge
        // but can be offset
        var angle = CGFloat(rotationOffset)
        
        let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle), y: center.y + (radius - cornerRadius) * sin(angle))
        path.move(to: CGPoint(x: corner.x + cornerRadius * cos(angle + theta), y: corner.y + cornerRadius * sin(angle + theta)))
        
        for _ in 0..<sides {
            angle += theta
            
            let corner = CGPoint(x: center.x + (radius - cornerRadius) * cos(angle), y: center.y + (radius - cornerRadius) * sin(angle))
            let tip = CGPoint(x: center.x + radius * cos(angle), y: center.y + radius * sin(angle))
            let start = CGPoint(x: corner.x + cornerRadius * cos(angle - theta), y: corner.y + cornerRadius * sin(angle - theta))
            let end = CGPoint(x: corner.x + cornerRadius * cos(angle + theta), y: corner.y + cornerRadius * sin(angle + theta))
            
            path.addLine(to: start)
            path.addQuadCurve(to: end, controlPoint: tip)
        }
        
        path.close()
        
        // Move the path to the correct origins
        let bounds = path.bounds
        let transform = CGAffineTransform(translationX: -bounds.origin.x + rect.origin.x + lineWidth / 2.0, y: -bounds.origin.y + rect.origin.y + lineWidth / 2.0)
        path.apply(transform)
        
        return path
    }
}

Could someone point me the right direction and what did I missed please? Thank you


Solution

  • When you are overriding draw -

    override func draw(_ rect: CGRect) {
    }
    

    AND

    You are not calling super.draw(rect) that means you won't get any of the UIButton's drawing.

    You can delete your draw(_ rect: CGRect) implementation and move you shape layer code into layoutSubviews() method instead.

    Also you should not keep adding a new layer every time this is called. Consider one of the following -

    1. Add shape layer only once and update it's frame every time it is called.

    2. Add new shape layer every time (remove the old instance before adding a new one).

    This will get you the UIButton's built-in drawing back.

    Also when you are adding a layer, you are calling addSublayer that will add your sublayer on top of everything else in the button's layer hierarchy covering up anything rendered by button's default drawing completely.

    Consider using insertSublayer variants - at: index OR below: sublayer etc.