Simulating a badge on a UISegmentedControl that is being used as a navigation item

Viewed 1365

I have a UIViewController that is embedded in a UINavigationController. The navigation item for this view controller is a UISegmentedControl with 3 segments. I'm trying to find a way to add a "new" badge to each of the segments. It appears that UISegmentedControl does not normally allow you to do this but I was thinking that for my purposes, perhaps I could simulate this using a custom UIView positioned at the left or right edge of each segment. I know I can get the width of the UISegmentedControl and since the auto-size mode is set to "equal widths", it seems reasonable that I could simply divide the total width by 3 to determine the approximate width of each segment.

However, there are a couple of things that I'm not sure about:

  1. Is it possible to determine the x/y position of the UISegmentedControl within the navigation bar so I know where to position the custom view(s)?
  2. Is it then possible to add a custom view at these positions inside the space contained by the navigation bar?
2 Answers

Add Views above the Segment Control, put constraint. enter image description here

In ViewDidLoad() use the code

 self.badge1.addSubview(self.addCounter(count: 0))
 self.badge2.addSubview(self.addCounter(count: 9))

Use this function to make badge counter

func addCounter(count: Int)->UIView {
    // Count > 0, show count
    if count > 0 {

        // Create label
        let fontSize: CGFloat = 10
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: fontSize)
        label.textAlignment = .center
        label.textColor = .white
        label.backgroundColor = .red

        // Add count to label and size to fit
        label.text = "\(NSNumber(value: count))"
        label.sizeToFit()

        // Adjust frame to be square for single digits or elliptical for numbers > 9
        var frame: CGRect = label.frame
        frame.size.height += CGFloat(Int(0.4 * fontSize))
        frame.size.width = (count <= 9) ? frame.size.height : frame.size.width + CGFloat(Int(fontSize))
        label.frame = frame

        // Set radius and clip to bounds
        label.layer.cornerRadius = frame.size.height / 2.0
        label.clipsToBounds = true

        // Show label in accessory view and remove disclosure
        return label

    } else {
        return UIView()
    }
}

And final result enter image description here

For those wanting to access the image views via the index, I created an extension, but it's not ideal, as it has to check whether the image data is equal, but works so far for me

extension UISegmentedControl {
    func getImageView(at index: Int) -> UIImageView? {
        guard let image = imageForSegment(at: index) else { return nil }
        let imageViews = subviews.compactMap { $0.subviews.first(where: { $0 is UIImageView }) as? UIImageView }
        return imageViews.first(where: {
            return $0.image?.isEqualTo(image: image) == true
        })
    }
}

extension UIImage {
    func isEqualTo(image: UIImage) -> Bool {
        guard let data1 = image.pngData() else { return false }
        guard let data2 = self.pngData() else { return false }

       return data1 == data2
    }
}
Related