Search code examples
iosswiftuisegmentedcontrol

The custom bottom bar is not at the correct position for a custom segmented control


I am trying to create a custom UISegmentedControl which has a bar at the bottom below the selected option. I have added a CALayer to act as the bar. After the selected option is changed, the bar does not move to its expected position

The code for the custom UISegmentedControl is as follows-

class ProfileSegmentedControl: UISegmentedControl {
    private lazy var bottomBar = getBottomBar()

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }
}

private extension ProfileSegmentedControl {
    func setup() {
        tintColor = .clear
        let normalAttributes: [NSAttributedString.Key: Any] = [
            .foregroundColor: UIColor.white,
            .font: UIFont.systemFont(ofSize: 16, weight: .medium)
        ]
        setTitleTextAttributes(normalAttributes, for: .normal)
        setTitleTextAttributes([.foregroundColor: UIColor(named: "black") as Any], for: .selected)
    }
}

private extension ProfileSegmentedControl {
    func getBottomBar() -> CALayer {
        let bar = CALayer()
        bar.backgroundColor = UIColor(named: "black")?.cgColor
        layer.addSublayer(bar)
        return bar
    }

    func setBarFrame() {
        let barWidth = bounds.width / CGFloat(numberOfSegments)
        let barHeight: CGFloat = 2
        let x = barWidth * CGFloat(selectedSegmentIndex)
        let y = bounds.height - barHeight

        bottomBar.frame = CGRect(x: x, y: y, width: barWidth, height: barHeight)
    }
}

extension ProfileSegmentedControl {
    override func layoutIfNeeded() {
        super.layoutIfNeeded()
        setBarFrame()
    }
}

It takes 2 taps on an option to move the bottom bar to the expected position.

Can anyone point out why this is happening? Can anyone point out how to fix this?

Edit- When the view is first loaded and "Segment 0" is tapped, the bar appears at the correct position as shown below-

enter image description here

When the "Segment 1" is tapped once, the bar does not move as shown below-

enter image description here

When the "Segment 1" is tapped again, the bar moves to the correct position as shown below-

enter image description here


Solution

  • I don't think this approach will work, because selectedSegmentIndex is not yet set when layoutIfNeeded() is called. That's why you're getting Seg-2 underlined when tapping Seg-1 and visa-versa.

    I'd suggest replacing your override of layoutIfNeeded() with:

    extension ProfileSegmentedControl {
    
        override func layoutSubviews() {
            super.layoutSubviews()
            setBarFrame()
        }
    
    }
    

    and then adding a valueChanged action:

    @IBAction func segmentChanged(_ sender: Any) {
        if let segControl = sender as? ProfileSegmentedControl {
            segControl.setNeedsLayout()
        }
    }
    

    which, presumably, you're implementing anyway to take action when a segment is tapped.

    This has the added advantage of auto-sizing the bar when the control width changes - such as on device rotation.