148

A UISegmentedControl has a new appearance in iOS 13 and existing code to alter the colors of the segmented control no longer work as they did.

Prior to iOS 13 you could set the tintColor and that would be used for the border around the segmented control, the lines between the segments, and the background color of the selected segment. Then you could change the color of the titles of each segment using the foreground color attribute with titleTextAttributes.

Under iOS 13, the tintColor does nothing. You can set the segmented control's backgroundColor to change the overall color of the segmented control. But I can't find any way to alter the color used as the background of the selected segment. Setting the text attributes still works. I even tried setting the background color of the title but that only affects the background of the title, not the rest of the selected segment's background color.

In short, how do you modify the background color of the currently selected segment of a UISegmentedControl in iOS 13? Is there a proper solution, using public APIs, that doesn't require digging into the private subview structure?

There are no new properties in iOS 13 for UISegmentedControl or UIControl and none of the changes in UIView are relevant.

rmaddy
  • 314,917
  • 42
  • 532
  • 579

17 Answers17

174

As of iOS 13b3, there is now a selectedSegmentTintColor on UISegmentedControl.

To change the overall color of the segmented control use its backgroundColor.

To change the color of the selected segment use selectedSegmentTintColor.

To change the color/font of the unselected segment titles, use setTitleTextAttributes with a state of .normal/UIControlStateNormal.

To change the color/font of the selected segment titles, use setTitleTextAttributes with a state of .selected/UIControlStateSelected.

If you create a segmented control with images, if the images are created as template images, then the segmented control's tintColor will be used to color the images. But this has a problem. If you set the tintColor to the same color as selectedSegmentTintColor then the image won't be visible in the selected segment. If you set the tintColor to the same color as backgroundColor, then the images on the unselected segments won't be visible. This means your segmented control with images must use 3 different colors for everything to be visible. Or you can use non-template images and not set the tintColor.

Under iOS 12 or earlier, simply set the segmented control's tintColor or rely on the app's overall tint color.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
  • How do we set the segment controller without borders? I see no setting for this in iOS 13. Earlier, setting tintcolor was enough to get a borderless segment control. – Deepak Sharma Aug 11 '19 at 14:06
  • Please add Border Color etc. so all can find all segment Color related issues are resolved here . what say's ? :) – Yogesh Patel Sep 18 '19 at 05:59
  • 1
    @YogeshPatel What about the border color? There is no border color in iOS 13 and in iOS 12 it's set with `tintColor` which is already covered in the answer. – rmaddy Sep 18 '19 at 06:01
  • @rmaddy I have set this [segmentedControl.layer setBorderColor:[[UIColor whiteColor] CGColor]]; [segmentedControl.layer setBorderWidth:0.5]; it gives me border and border color in iOS 13. – Yogesh Patel Sep 18 '19 at 06:04
  • 1
    Oh, that border. That applies to any view, not just a segmented control. That's beyond the scope of the original question and this answer. Your comment is enough. – rmaddy Sep 18 '19 at 06:05
  • 1
    I dont know why the backgroundColor is not work when i use UIColor.white but it work when i use UIColor.red or other color? – Mohammed Abunada Sep 24 '19 at 15:15
  • @MohammedAbunada You should post your own question with all relevant details. – rmaddy Sep 24 '19 at 15:35
  • It is worth to add, that if you set just the colours, the output will be WRONG! That's because the overlaying ImageView is a bit grey (try to set `backgroundColor` to `.black` and inspect color - it will be `15,15,15`). You have to override background image with a not-nil image to get rid of that grey mask above it. The bug happens at least up to iOS 13.2.2. – Nat Nov 20 '19 at 01:18
  • This has no effect in a share extension, strange... – jjxtra Sep 17 '22 at 17:54
67

IOS 13 and Swift 5.0 (Xcode 11.0)Segment Control 100% Working

enter image description here

enter image description here

 if #available(iOS 13.0, *) {
      yoursegmentedControl.backgroundColor = UIColor.black
      yoursegmentedControl.layer.borderColor = UIColor.white.cgColor
      yoursegmentedControl.selectedSegmentTintColor = UIColor.white
      yoursegmentedControl.layer.borderWidth = 1

      let titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]    
      yoursegmentedControl.setTitleTextAttributes(titleTextAttributes, for:.normal)

      let titleTextAttributes1 = [NSAttributedString.Key.foregroundColor: UIColor.black]
      yoursegmentedControl.setTitleTextAttributes(titleTextAttributes1, for:.selected)
  } else {
              // Fallback on earlier versions
}
Maulik Patel
  • 2,045
  • 17
  • 24
58

As of Xcode 11 beta 3

There is now the selectedSegmentTintColor property on UISegmentedControl.

See rmaddy's answer


To get back iOS 12 appearance

I wasn't able to tint the color of the selected segment, hopefully it will be fixed in an upcoming beta.

Setting the background image of the selected state doesn't work without setting the background image of the normal state (which removes all the iOS 13 styling)

But I was able to get it back to the iOS 12 appearance (or near enough, I wasn't able to return the corner radius to its smaller size).

It's not ideal, but a bright white segmented control looks a bit out of place in our app.

(Didn't realise UIImage(color:) was an extension method in our codebase. But the code to implement it is around the web)

extension UISegmentedControl {
    /// Tint color doesn't have any effect on iOS 13.
    func ensureiOS12Style() {
        if #available(iOS 13, *) {
            let tintColorImage = UIImage(color: tintColor)
            // Must set the background image for normal to something (even clear) else the rest won't work
            setBackgroundImage(UIImage(color: backgroundColor ?? .clear), for: .normal, barMetrics: .default)
            setBackgroundImage(tintColorImage, for: .selected, barMetrics: .default)
            setBackgroundImage(UIImage(color: tintColor.withAlphaComponent(0.2)), for: .highlighted, barMetrics: .default)
            setBackgroundImage(tintColorImage, for: [.highlighted, .selected], barMetrics: .default)
            setTitleTextAttributes([.foregroundColor: tintColor, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 13, weight: .regular)], for: .normal)
            setDividerImage(tintColorImage, forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default)
            layer.borderWidth = 1
            layer.borderColor = tintColor.cgColor
        }
    }
}

Image showing the effect of the above code

Jonathan.
  • 53,997
  • 54
  • 186
  • 290
  • This could be a good workaround. I haven't had a chance to try this yet but does this also require a call to `setTitleTextAttributes` to make the selected segment's title have a white color? – rmaddy Jun 05 '19 at 15:36
  • Hmm, it looks like it should but seemingly not. I haven't got access to that code usage atm, but the image on the left is created with that code. – Jonathan. Jun 05 '19 at 22:46
  • This is a great solution if you want to keep the iOS 12 look but as of iOS 13b3, there is now the `selectedSegmentTintColor` property on `UISegmentedControl`. – rmaddy Jul 03 '19 at 17:19
  • Ah that's awesome, don't know why they didn't just use tintColor which dies nothing. – Jonathan. Jul 04 '19 at 08:03
  • @Jonathan. `tintColor` is used if the segmented control is made with images instead of titles and the images are template images. – rmaddy Jul 08 '19 at 17:08
  • 9
    https://stackoverflow.com/a/33675160/5790492 for UIImage(color:) extension. – Nike Kov Sep 15 '19 at 21:28
  • Nice answer, works! Pay attention that this code must NOT be in `layoutSubviews`. – Nike Kov Sep 16 '19 at 07:39
  • 1
    It would be nice if you would add the UIImage extension, this way, your answer is not complete iho – FredFlinstone Oct 14 '19 at 13:19
  • Is there a way to do this with Objective-C? I'm dealing with an older codebase and seems like Obj-c extensions only work if you have the source code. – Alyoshak Oct 23 '19 at 13:11
  • @Alyoshak see Colin Blakes answer https://stackoverflow.com/a/56465501/191463 – Jonathan. Oct 25 '19 at 10:01
  • It's interesting why do we setBackgroundImage for .selected and .highlighted states twice? First time separately and then together - [.highlighted, .selected] – Vitya Shurapov Dec 04 '19 at 10:19
  • 2
    @VityaShurapov its setting it for when it's both highlighted *and* selected, it's not an array of states passed in but rather an option set, which means the values are combined to create a new state. – Jonathan. Dec 04 '19 at 11:46
  • How to reduce the separator thickness?? – Midhun Narayan May 25 '21 at 03:42
  • It seems that this method does not affect the colors when some images are used for selected and non-selected values (instead of text). Moreover, tint color still has a function: the segment images are always drawn with the tint color, and selectedSegmentTintColor does not work for them. – Ugur Oct 31 '21 at 08:19
  • did you try it by yourself? It makes UISegmentedControl's height equal to 1px – Gargo Jul 17 '23 at 06:58
21

Swift version of @Ilahi Charfeddine answer:

if #available(iOS 13.0, *) {
   segmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
   segmentedControl.selectedSegmentTintColor = UIColor.blue
} else {
   segmentedControl.tintColor = UIColor.blue
}
Vignan Sankati
  • 380
  • 5
  • 15
20

I've tried the workaround and it works great for me. Here's the Objective-C version:

@interface UISegmentedControl (Common)
- (void)ensureiOS12Style;
@end
@implementation UISegmentedControl (Common)
- (void)ensureiOS12Style {
    // UISegmentedControl has changed in iOS 13 and setting the tint
    // color now has no effect.
    if (@available(iOS 13, *)) {
        UIColor *tintColor = [self tintColor];
        UIImage *tintColorImage = [self imageWithColor:tintColor];
        // Must set the background image for normal to something (even clear) else the rest won't work
        [self setBackgroundImage:[self imageWithColor:self.backgroundColor ? self.backgroundColor : [UIColor clearColor]] forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
        [self setBackgroundImage:tintColorImage forState:UIControlStateSelected barMetrics:UIBarMetricsDefault];
        [self setBackgroundImage:[self imageWithColor:[tintColor colorWithAlphaComponent:0.2]] forState:UIControlStateHighlighted barMetrics:UIBarMetricsDefault];
        [self setBackgroundImage:tintColorImage forState:UIControlStateSelected|UIControlStateSelected barMetrics:UIBarMetricsDefault];
        [self setTitleTextAttributes:@{NSForegroundColorAttributeName: tintColor, NSFontAttributeName: [UIFont systemFontOfSize:13]} forState:UIControlStateNormal];
        [self setDividerImage:tintColorImage forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
        self.layer.borderWidth = 1;
        self.layer.borderColor = [tintColor 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;
}
@end
Cœur
  • 37,241
  • 25
  • 195
  • 267
Colin Blake
  • 319
  • 1
  • 5
  • 2
    I'm not sure that it will work with `CGRectMake(0.0f, 0.0f, 1.0f, 1.0f)`: from my tests with Xcode 11 beta, `rect` had to be the same size as the bounds of the segmented control. – Cœur Jun 16 '19 at 17:18
  • 2
    Since iOS13 beta 6 the tintcolor did not show up on a selected button, so I had to add a line: [self setTitleTextAttributes:@{NSForegroundColorAttributeName: UIColor.blackColor, NSFontAttributeName: [UIFont systemFontOfSize:13]} forState:UIControlStateSelected]; – Peter Johnson Aug 23 '19 at 16:27
  • When I try to use this on `[[UISegmentedControl appearance] ensureiOS12Style]` I get an exception. Any idea what is going on? `Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSMethodSignature getArgumentTypeAtIndex:]: index (2) out of bounds [0, 1]'` – Jeremy Hicks Jun 19 '20 at 20:52
  • This has no effect in a share extension for some reason... – jjxtra Sep 17 '22 at 17:53
15

As of Xcode 11 beta 3

There is now the selectedSegmentTintColor property on UISegmentedControl.

Thank you @rmaddy!


Original answer, for Xcode 11 beta and beta 2

Is there a proper solution, using public APIs, that doesn't require digging into the private subview structure?

With Xcode 11.0 beta, it seems to be a challenge to do it by-the-rules, because it basically requires to redraw all the background images for every states by yourself, with round corners, transparency and resizableImage(withCapInsets:). For instance, you would need to generate a colored image similar to:
enter image description here

So for now, the let's-dig-into-the-subviews way seems much easier:

class TintedSegmentedControl: UISegmentedControl {

    override func layoutSubviews() {
        super.layoutSubviews()

        if #available(iOS 13.0, *) {
            for subview in subviews {
                if let selectedImageView = subview.subviews.last(where: { $0 is UIImageView }) as? UIImageView,
                    let image = selectedImageView.image {
                    selectedImageView.image = image.withRenderingMode(.alwaysTemplate)
                    break
                }
            }
        }
    }
}

This solution will correctly apply the tint color to the selection, as in: enter image description here

Cœur
  • 37,241
  • 25
  • 195
  • 267
  • 1
    Awarded because the time is running out, but preferably a solution that doesn't involve digging into private hierarchy will be found :) – Jonathan. Jun 18 '19 at 07:58
  • @Jonathan. Thank you. You already got the closest solution not involving looking at the hierarchy: because once you `setBackgroundImage` for `.normal`, you have to set all the other images (including other states and `setDividerImage`), probably with some `UIBezierPath` and `resizableImage(withCapInsets:)`, which makes it overly complex if we want the iOS 13 design this way. – Cœur Jun 18 '19 at 08:54
  • Yup, ideally it will be fixed in a beta – Jonathan. Jun 18 '19 at 10:30
  • 3
    This is no longer needed as of iOS 13b3. There is now the `selectedSegmentTintColor` property on `UISegmentedControl`. – rmaddy Jul 03 '19 at 17:16
14

iOS13 UISegmentController

how to use:

segment.setOldLayout(tintColor: .green)

extension UISegmentedControl
{
    func setOldLayout(tintColor: UIColor)
    {
        if #available(iOS 13, *)
        {
            let bg = UIImage(color: .clear, size: CGSize(width: 1, height: 32))
             let devider = UIImage(color: tintColor, size: CGSize(width: 1, height: 32))

             //set background images
             self.setBackgroundImage(bg, for: .normal, barMetrics: .default)
             self.setBackgroundImage(devider, for: .selected, barMetrics: .default)

             //set divider color
             self.setDividerImage(devider, forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default)

             //set border
             self.layer.borderWidth = 1
             self.layer.borderColor = tintColor.cgColor

             //set label color
             self.setTitleTextAttributes([.foregroundColor: tintColor], for: .normal)
             self.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
        }
        else
        {
            self.tintColor = tintColor
        }
    }
}
extension UIImage {
    convenience init(color: UIColor, size: CGSize) {
        UIGraphicsBeginImageContextWithOptions(size, false, 1)
        color.set()
        let ctx = UIGraphicsGetCurrentContext()!
        ctx.fill(CGRect(origin: .zero, size: size))
        let image = UIGraphicsGetImageFromCurrentImageContext()!
        UIGraphicsEndImageContext()

        self.init(data: image.pngData()!)!
    }
}
Jigar
  • 1,801
  • 1
  • 14
  • 29
  • 1
    This is the only approach that worked for me - the background images. selectedSegmentTintColor did not work for some reason. – DenNukem Mar 27 '20 at 17:13
  • Amazing! Still works for ios 14 and lower – Lito Jan 16 '21 at 20:56
  • This is great for getting rid of the default grey background colour. @DenNukem to get the segment tint color ```let selected = UIImage(color: .systemPink, size: CGSize(width: 1, height: 32)) UISegmentedControl.appearance().setBackgroundImage(selected, for: .selected, barMetrics: .default)``` is working for me – sally2000 Apr 23 '21 at 09:18
  • Using iOS 14.5, the `selectedSegmentTintColor` is not respected – CyberMew Nov 08 '21 at 06:37
10
if (@available(iOS 13.0, *)) {

    [self.segmentedControl setTitleTextAttributes:@{NSForegroundColorAttributeName: [UIColor whiteColor], NSFontAttributeName: [UIFont systemFontOfSize:13]} forState:UIControlStateSelected];
    [self.segmentedControl setSelectedSegmentTintColor:[UIColor blueColor]];

} else {

[self.segmentedControl setTintColor:[UIColor blueColor]];}
8

XCODE 11.1 & iOS 13

Based on @Jigar Darji 's answer but a safer implementation.

We first create a failable convenience initialiser:

extension UIImage {

convenience init?(color: UIColor, size: CGSize) {
    UIGraphicsBeginImageContextWithOptions(size, false, 1)
    color.set()
    guard let ctx = UIGraphicsGetCurrentContext() else { return nil }
    ctx.fill(CGRect(origin: .zero, size: size))
    guard
        let image = UIGraphicsGetImageFromCurrentImageContext(),
        let imagePNGData = image.pngData()
        else { return nil }
    UIGraphicsEndImageContext()

    self.init(data: imagePNGData)
   }
}

Then we extend UISegmentedControl:

extension UISegmentedControl {

func fallBackToPreIOS13Layout(using tintColor: UIColor) {
    if #available(iOS 13, *) {
        let backGroundImage = UIImage(color: .clear, size: CGSize(width: 1, height: 32))
        let dividerImage = UIImage(color: tintColor, size: CGSize(width: 1, height: 32))

        setBackgroundImage(backGroundImage, for: .normal, barMetrics: .default)
        setBackgroundImage(dividerImage, for: .selected, barMetrics: .default)

        setDividerImage(dividerImage,
                        forLeftSegmentState: .normal,
                        rightSegmentState: .normal, barMetrics: .default)

        layer.borderWidth = 1
        layer.borderColor = tintColor.cgColor

        setTitleTextAttributes([.foregroundColor: tintColor], for: .normal)
        setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
    } else {
        self.tintColor = tintColor
    }
  }
}
FredFlinstone
  • 896
  • 11
  • 16
6

While answers above are great, most of them get the color of text inside selected segment wrong. I've created UISegmentedControl subclass which you can use on iOS 13 and pre-iOS 13 devices and use the tintColor property as you would on pre iOS 13 devices.

    class LegacySegmentedControl: UISegmentedControl {
        private func stylize() {
            if #available(iOS 13.0, *) {
                selectedSegmentTintColor = tintColor
                let tintColorImage = UIImage(color: tintColor)
                setBackgroundImage(UIImage(color: backgroundColor ?? .clear), for: .normal, barMetrics: .default)
                setBackgroundImage(tintColorImage, for: .selected, barMetrics: .default)
                setBackgroundImage(UIImage(color: tintColor.withAlphaComponent(0.2)), for: .highlighted, barMetrics: .default)
                setBackgroundImage(tintColorImage, for: [.highlighted, .selected], barMetrics: .default)
                setTitleTextAttributes([.foregroundColor: tintColor!, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 13, weight: .regular)], for: .normal)

                setDividerImage(tintColorImage, forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default)
                layer.borderWidth = 1
                layer.borderColor = tintColor.cgColor

// Detect underlying backgroundColor so the text color will be properly matched

                if let background = backgroundColor {
                    self.setTitleTextAttributes([.foregroundColor: background, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 13, weight: .regular)], for: .selected)
                } else {
                    func detectBackgroundColor(of view: UIView?) -> UIColor? {
                        guard let view = view else {
                            return nil
                        }
                        if let color = view.backgroundColor, color != .clear {
                            return color
                        }
                        return detectBackgroundColor(of: view.superview)
                    }
                    let textColor = detectBackgroundColor(of: self) ?? .black

                    self.setTitleTextAttributes([.foregroundColor: textColor, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 13, weight: .regular)], for: .selected)
                }
            }
        }

        override func tintColorDidChange() {
            super.tintColorDidChange()
            stylize()
        }
    }

    fileprivate extension UIImage {
        public convenience init?(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) {
          let rect = CGRect(origin: .zero, size: size)
          UIGraphicsBeginImageContextWithOptions(rect.size, false, 0.0)
          color.setFill()
          UIRectFill(rect)
          let image = UIGraphicsGetImageFromCurrentImageContext()
          UIGraphicsEndImageContext()

          guard let cgImage = image?.cgImage else { return nil }
          self.init(cgImage: cgImage)
        }
    }

Using tintColorDidChange method we ensure that the stylize method will be called every time the tintColor property changes on the segment view, or any of the underlying views, which is preferred behaviour on iOS.

Result: enter image description here

Adam
  • 1,776
  • 1
  • 17
  • 28
6

The SwiftUI Picker lacks some basic options. For people trying to customize a Picker with SegmentedPickerStyle() in SwiftUI in iOS 13 or 14, the simplest option is to use UISegmentedControl.appearance() to set the appearance globally. Here is an example function that could be called to set the appearance.

func setUISegmentControlAppearance() {
    UISegmentedControl.appearance().selectedSegmentTintColor = .white
    UISegmentedControl.appearance().backgroundColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.1)
    UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.black], for: .normal)
    UISegmentedControl.appearance().setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
}

However, globally setting appearance options with UISegmentedControl.appearance() is not great if you want multiple controls with different settings. Another option is to implement UIViewRepresentable for UISegmentedControl. Here is an example that sets the properties asked about in the the original question and sets .apportionsSegmentWidthsByContent = true as a bonus. Hopefully this will save you some time...

struct MyPicker: UIViewRepresentable {

    @Binding var selection: Int // The type of selection may vary depending on your use case
    var items: [Any]?

    class Coordinator: NSObject {
        let parent: MyPicker
        init(parent: MyPicker) {
            self.parent = parent
        }

        @objc func valueChanged(_ sender: UISegmentedControl) {
            self.parent.selection = Int(sender.selectedSegmentIndex)
        }
    }

    func makeCoordinator() -> MyPicker.Coordinator {
        Coordinator(parent: self)
    }

    func makeUIView(context: Context) -> UISegmentedControl {
        let picker = UISegmentedControl(items: self.items)

        // Any number of other UISegmentedControl settings can go here
        picker.selectedSegmentTintColor = .white
        picker.backgroundColor = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.1)
        picker.setTitleTextAttributes([.foregroundColor: UIColor.black], for: .normal)
        picker.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
        picker.apportionsSegmentWidthsByContent = true

        // Make sure the coordinator updates the picker when the value changes
        picker.addTarget(context.coordinator, action: #selector(Coordinator.valueChanged(_:)), for: .valueChanged)

        return picker
    }

    func updateUIView(_ uiView: UISegmentedControl, context: Context) {
        uiView.selectedSegmentIndex = self.selection
    }
 }
user1248465
  • 224
  • 3
  • 6
5

Here is my take on Jonathan.'s answer for Xamarin.iOS (C#), but with fixes for image sizing. As with Cœur's comment on Colin Blake's answer, I made all images except the divider the size of the segmented control. The divider is 1xheight of the segment.

public static UIImage ImageWithColor(UIColor color, CGSize size)
{
    var rect = new CGRect(0, 0, size.Width, size.Height);
    UIGraphics.BeginImageContext(rect.Size);
    var context = UIGraphics.GetCurrentContext();
    context.SetFillColor(color.CGColor);
    context.FillRect(rect);
    var image = UIGraphics.GetImageFromCurrentImageContext();
    UIGraphics.EndImageContext();
    return image;
}

// https://stackoverflow.com/a/56465501/420175
public static void ColorSegmentiOS13(UISegmentedControl uis, UIColor tintColor, UIColor textSelectedColor, UIColor textDeselectedColor)
{
    if (!UIDevice.CurrentDevice.CheckSystemVersion(13, 0))
    {
        return;
    }

    UIImage image(UIColor color)
    {
        return ImageWithColor(color, uis.Frame.Size);
    }

    UIImage imageDivider(UIColor color)
    {
        return ImageWithColor(color, 1, uis.Frame.Height);
    }

    // Must set the background image for normal to something (even clear) else the rest won't work
    //setBackgroundImage(UIImage(color: backgroundColor ?? .clear), for: .normal, barMetrics: .default)
    uis.SetBackgroundImage(image(UIColor.Clear), UIControlState.Normal, UIBarMetrics.Default);

    // setBackgroundImage(tintColorImage, for: .selected, barMetrics: .default)
    uis.SetBackgroundImage(image(tintColor), UIControlState.Selected, UIBarMetrics.Default);

    // setBackgroundImage(UIImage(color: tintColor.withAlphaComponent(0.2)), for: .highlighted, barMetrics: .default)
    uis.SetBackgroundImage(image(tintColor.ColorWithAlpha(0.2f)), UIControlState.Highlighted, UIBarMetrics.Default);

    // setBackgroundImage(tintColorImage, for: [.highlighted, .selected], barMetrics: .default)
    uis.SetBackgroundImage(image(tintColor), UIControlState.Highlighted | UIControlState.Selected, UIBarMetrics.Default);

    // setTitleTextAttributes([.foregroundColor: tintColor, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 13, weight: .regular)], for: .normal)
    // Change: support distinct color for selected/de-selected; keep original font
    uis.SetTitleTextAttributes(new UITextAttributes() { TextColor = textDeselectedColor }, UIControlState.Normal); //Font = UIFont.SystemFontOfSize(13, UIFontWeight.Regular)
    uis.SetTitleTextAttributes(new UITextAttributes() { TextColor = textSelectedColor, }, UIControlState.Selected); //Font = UIFont.SystemFontOfSize(13, UIFontWeight.Regular)

    // setDividerImage(tintColorImage, forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default)
    uis.SetDividerImage(imageDivider(tintColor), UIControlState.Normal, UIControlState.Normal, UIBarMetrics.Default);

    //layer.borderWidth = 1
    uis.Layer.BorderWidth = 1;

    //layer.borderColor = tintColor.cgColor
    uis.Layer.BorderColor = tintColor.CGColor;
}
Cœur
  • 37,241
  • 25
  • 195
  • 267
t9mike
  • 1,546
  • 2
  • 19
  • 31
5

You can implement following method

extension UISegmentedControl{
    func selectedSegmentTintColor(_ color: UIColor) {
        self.setTitleTextAttributes([.foregroundColor: color], for: .selected)
    }
    func unselectedSegmentTintColor(_ color: UIColor) {
        self.setTitleTextAttributes([.foregroundColor: color], for: .normal)
    }
}

Usage code

segmentControl.unselectedSegmentTintColor(.white)
segmentControl.selectedSegmentTintColor(.black)
Suraj Rao
  • 29,388
  • 11
  • 94
  • 103
Zain Ul Abideen
  • 693
  • 10
  • 15
2

If you want to set the background to clear you have to do this:

if #available(iOS 13.0, *) {
  let image = UIImage()
  let size = CGSize(width: 1, height: segmentedControl.intrinsicContentSize.height)
  UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
  image.draw(in: CGRect(origin: .zero, size: size))
  let scaledImage = UIGraphicsGetImageFromCurrentImageContext()
  UIGraphicsEndImageContext()
  segmentedControl.setBackgroundImage(scaledImage, for: .normal, barMetrics: .default)
  segmentedControl.setDividerImage(scaledImage, forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default)
}

Result looks like: enter image description here

Cameron Porter
  • 801
  • 6
  • 15
  • 1
    Almost working, but I am on iOS 14 and the `selectedSegmentTintColor` does not show up (i.e. selected segment option is white and not my selected color). – CyberMew Nov 08 '21 at 06:43
2

In iOS 13 and above, when I accessed the segmented control's subviews in viewDidLoad, it has 1 UIImageView. Once I inserted more segments, it increases respectively, so 3 segments means 3 UIImageView as the segmented control's subviews.

Interestingly, by the time it reaches viewDidAppear, the segmented control's subviews became 3 UISegment (each contained a UISegmentLabel as its subview; this is your text) and 4 UIImageView as shown below:

viewDidAppear

Those 3 UIImageView in viewDidLoad somehow become UISegment, and we don't want to touch them (but you can try to set their image or isHidden just to see how it affects the UI). Let's ignore these private classes.

Those 4 UIImageView are actually 3 "normal" UIImageView (together with 1 UIImageView subview as the vertical separator) and 1 "selected" UIImageView (i.e. this is actually your selectedSegmentTintColor image, notice in the screenshot above there are no subviews under it).

In my case, I needed a white background, so I had to hide the greyish background images (see: https://medium.com/flawless-app-stories/ios-13-uisegmentedcontrol-3-important-changes-d3a94fdd6763). I also wanted to remove/hide the vertical separators between the segments.

Hence the simple solution in viewDidAppear, without needed to set divider image or background image (in my case), is to simply hide those first 3 UIImageView:

// This method might be a bit 'risky' since we are not guaranteed of the internal sequence ordering, but so far it seems ok.
if #available(iOS 13.0, *) {
    for i in 0...(segmentedControl.numberOfSegments - 1) {
        segmentedControl.subviews[i].isHidden = true
    }
}

or

// This does not depend on the ordering sequence like the above, but this is also risky in the sense that if the UISegment becomes UIImageView one day, this will break.
if #available(iOS 13.0, *) {
    for subview in segmentedControl.subviews {
        if String(describing: subview).contains("UIImageView"),
           subview.subviews.count > 0 {
               subview.isHidden = true
        }
    }
}

Pick your poison...

CyberMew
  • 1,159
  • 1
  • 16
  • 33
  • I have also reported a bug under FB9746071 when setting background color to white gives me grey apperance (`segmentedControl.backgroundColor = UIColor.white`). – CyberMew Apr 04 '22 at 09:33
1

Gray background in iOS 13+ solution

Since iOS 13 there is introduced updated UISegmentedControl. Unfortunately there is no simple way to setup background to white, as framework is adding semitransparent gray background. In the result, background of UISegmentedControl remains gray. There is workaround for it (ugly, but working):

func fixBackgroundColorWorkaround() {
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
        for i in 0 ... (self.numberOfSegments-1) {
            let bg = self.subviews[i]
            bg.isHidden = true
        }
    }
}
lukszar
  • 1,252
  • 10
  • 13
-2

iOS 12+

I struggled a lot for background color. Setting .clear color as background color always added default grey color. Here is how i fixed

self.yourSegmentControl.backgroundColor = .clear //Any Color of your choice

self.yourSegmentControl.setBackgroundImage(UIImage(), for: .normal, barMetrics: .default) //This does the magic

Additionally for Divider Color

self.yourSegmentControl.setDividerImage(UIImage(), forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default) // This will remove the divider image.
Kedar Sukerkar
  • 1,410
  • 1
  • 16
  • 22