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
There is now the selectedSegmentTintColor property on
UISegmentedControl
.
See rmaddy's answer
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
}
}
}
IOS 13 and Swift 5.0 (Xcode 11.0)Segment Control 100% Working
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
}
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:
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;
}
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)
There is now the selectedSegmentTintColor property on
UISegmentedControl
.
Thank you @rmaddy!
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:
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: