6

I'm currently having problems with an iOS 13 segmented controller. I have this method in which I change the appearance of my segmented controller, it worked great until iOS 13 got out. now I the segmentedController has a grey background always, even if I set the background color to white, or black or whatever.

What Can I do?

- (void)modifySegmentedControl{
    if (@available(iOS 13.0, *)) {
        [_segmentedControl setBackgroundColor:UIColor.clearColor];
        [_segmentedControl setSelectedSegmentTintColor:UIColor.clearColor];
    } else {
         //I had this for <iOS13, it works great
        [_segmentedControl setBackgroundColor:UIColor.clearColor];
        [_segmentedControl setTintColor:UIColor.clearColor];
    }

    [_segmentedControl setTitleTextAttributes:
       [NSDictionary dictionaryWithObjectsAndKeys:
        [UIColor colorWithRed:0.25 green:0.25 blue:0.25 alpha:0.6], NSForegroundColorAttributeName,
        [UIFont fontWithName:@"Poppins-Medium" size:15.0], NSFontAttributeName,
       nil]
    forState: UIControlStateNormal];

    [_segmentedControl setTitleTextAttributes:
       [NSDictionary dictionaryWithObjectsAndKeys:
        [UIColor colorWithRed:0.25 green:0.25 blue:0.25 alpha:1.0], NSForegroundColorAttributeName,
        [UIFont fontWithName:@"Poppins-Medium" size:15.0], NSFontAttributeName,
       nil]
    forState: UIControlStateSelected];

    self->greenBar = [[UIView alloc] init];
//This needs to be false since we are using auto layout constraints
    [self->greenBar setTranslatesAutoresizingMaskIntoConstraints:NO];
    [self->greenBar setBackgroundColor:[UIColor colorWithRed:0.00 green:0.58 blue:0.27 alpha:1.0]]; //Kelley green
//
    [_vistaTable addSubview:self->greenBar];
//
    [self->greenBar.topAnchor constraintEqualToAnchor:_segmentedControl.bottomAnchor].active = YES;
    [self->greenBar.heightAnchor constraintEqualToConstant:3].active = YES;
    [self->greenBar.leftAnchor constraintEqualToAnchor:_segmentedControl.leftAnchor].active = YES;
    [self->greenBar.widthAnchor constraintEqualToAnchor:_segmentedControl.widthAnchor multiplier:0.5].active = YES;
}
Jalil
  • 1,167
  • 11
  • 34

3 Answers3

8

Try this:

if (@available(iOS 13.0, *)) {
    self.segmentedControl.selectedSegmentTintColor = UIColor.redColor;
    self.segmentedControl.layer.backgroundColor = UIColor.greenColor.CGColor;
}

.selectedSegmentTintColor defines the selected button color and .layer.backgroundColor the color for the whole UISegmentedControl background.

This is the result:

enter image description here

EDIT

It turns out this won't work for background clear or white, since on iOS 13 a sort of background image is added on the background and dividers of the segmented control:

enter image description here

A workaroud is create an image from color with UIGraphicsGetImageFromCurrentImageContext . The code looks like this:

- (void)viewDidLoad {
    [super viewDidLoad];

    if (@available(iOS 13.0, *)) {
        self.segmentedControl.selectedSegmentTintColor = UIColor.redColor;
        self.segmentedControl.layer.backgroundColor = UIColor.clearColor.CGColor;
        [self customizeSegmentedControlWithColor: UIColor.whiteColor];
    }
}

- (void)customizeSegmentedControlWithColor:(UIColor *)color {

    UIImage *tintColorImage = [self imageWithColor: color];
    [self.segmentedControl setBackgroundImage:[self imageWithColor:self.segmentedControl.backgroundColor ? self.segmentedControl.backgroundColor : [UIColor clearColor]] forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
    [self.segmentedControl setBackgroundImage:tintColorImage forState:UIControlStateSelected barMetrics:UIBarMetricsDefault];
    [self.segmentedControl setBackgroundImage:[self imageWithColor:[color colorWithAlphaComponent:0.2]] forState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault];
    [self.segmentedControl setBackgroundImage:tintColorImage forState:UIControlStateSelected|UIControlStateSelected barMetrics:UIBarMetricsDefault];
    [self.segmentedControl setDividerImage:tintColorImage forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
    self.segmentedControl.layer.borderWidth = 1;
    self.segmentedControl.layer.borderColor = [color CGColor];
}

- (UIImage *)imageWithColor: (UIColor *)color {
    CGRect rect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f);
    UIGraphicsBeginImageContext(rect.size);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSetFillColorWithColor(context, [color CGColor]);
    CGContextFillRect(context, rect);
    UIImage *theImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return theImage;
}

Check more about it here.

This is the result for background view with color and segmented control white:

enter image description here

And is the result for background view white and segmented control with color:

enter image description here

Marina Aguilar
  • 1,151
  • 9
  • 26
2

I found a way to do it based @MarinaAguilar's answer. I converted it to be Swift 5.7 compatible and tested it on iOS 16.5.1 (it will probably work for many other versions of both). It is interesting that all that is required is a 1 pixel image with the pixel set to the color you want in order to create a background of that color.

There are more control states that can be customized than I am showing here. Also the divider image could also be set and I'm not setting it. But this should be good enough to get the idea and an approach.

Note 1: I've seen a various approaches to creating a UIImage based on a color, but most of them are based on a deprecated function call. The one shown here is using the newer way of doing it with `UIGraphicsImageRenderer()` and implemented in a convenience initializer, for convenience.

Note 2: I chose the colors shown here at random out of my head rather than uses colors from my app. They probably are not aesthetically reasonable.
extension UIImage {
    convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
        let renderer = UIGraphicsImageRenderer(size: size)
        let image = renderer.image { (context) in
            color.setFill()
            context.fill(CGRect(origin: CGPoint.zero, size: size))
        }
        guard let pngData = image.pngData() else { return nil }
        self.init(data: pngData)
    }
}

func colorizeSegmentedControl(_ segControl: UISegmentedControl, color: UIColor, highlight: UIColor) {
    let tintColorImage = UIImage(color: color)
    let highlightedColorImage = UIImage(color: highlight)
    segControl.setBackgroundImage(tintColorImage, for: .normal, barMetrics: .default)
    segControl.setBackgroundImage(highlightedColorImage, for: .selected, barMetrics: .default)
    segControl.layer.borderWidth = 1
    segControl.layer.borderColor = color.cgColor
}

@objc func segControlStateChanged() { print("do something") }

func configureSegmentedControl(_ segControl: UISegmentedControl) {
    segControl = UISegmentedControl(items: ["item1", "item2"])
    segControl.translatesAutoresizingMaskIntoConstraints = false
    segControl.addTarget(self, action: #selector(segControlStateChanged), for: .primaryActionTriggered)
    segControl.layer.cornerRadius = 12
    segControl.clipsToBounds = true
    segControl.selectedSegmentIndex = 0
    colorizeSegmentedControl(segControl, color: .systemIndigo, highlight: .tertiaryLabel)
    view.addSubview(self.segControl)
}
clearlight
  • 12,255
  • 11
  • 57
  • 75
-1

You must hide the default UIImageViews that add that tint color:

class CustomSegmentControl: UISegmentedControl {

  override func layoutSubviews() {
    let imageViews = self.subviews.filter({ $0 is UIImageView }).compactMap({ $0 as? UIImageView })
      for imageView in Array(imageViews[..<self.numberOfSegments]) {
        imageView.isHidden = true
      }
  }

}

Tested and working on iOS 13.1 -> 16.5

Radu Ursache
  • 1,230
  • 1
  • 22
  • 35
  • This kind of stuff seems pretty risky because it creates a dependency on the hidden implementation which is undocumented and unsupported by Apple. If it's even allowed by the terms of agreement it is at risk for being changed without notice. – clearlight Jul 13 '23 at 04:41
  • @clearlight there is no real alternative to this other than building your custom `UISegmentedControl` from scratch. While it may break in the future, it still works on iOS 17 beta 3 and will work until Apple changes the `UISegmentedControl` behind the scenes at which point you will know about it. Native ways of doing things break more often than these "hidden implementations". Saying that the ToS is broken by iterating through a view is laughable and sad. – Radu Ursache Jul 13 '23 at 12:24
  • I'm not saying the TOS is violated, I am just erring on the side of caution. I haven't released enough apps to know what Apple would actually frown on or block submission about, I'm just paranoid about using undocumented interfaces, since Apple has at least some advice not to. I'm also not on my high horse about this, I agree we should have some means to get the look and feel we want want within reason, but the guidelines are trying to I guess achieve some uniformity and standards. Anyway I see a lot of tradeoffs. And it just 'feels' risky. – clearlight Jul 13 '23 at 17:07