如何在 iOS13的 UISegmentedControl 中更改段的颜色?

UISegmentedControl在 iOS13中有了新的外观,用于改变分段控件颜色的现有代码不再像以前那样起作用。

在 iOS13之前,你可以设置 tintColor,它将用于分段控件周围的边框、分段之间的线条以及选定分段的背景颜色。然后,您可以使用 titleTextAttributes的前景颜色属性更改每个片段的标题的颜色。

在 iOS13下,tintColor什么也做不了。可以设置分段控件的 backgroundColor以更改分段控件的整体颜色。但是我找不到任何方法来改变所选段的背景颜色。设置文本属性仍然有效。我甚至尝试设置标题的背景颜色,但这只会影响标题的背景,而不会影响所选片段的其余背景颜色。

简而言之,如何在 iOS13中修改当前选择的 UISegmentedControl片段的背景颜色?有没有一种使用公共 API 而不需要深入挖掘私有子视图结构的适当解决方案?

在 iOS13中,对于 UISegmentedControlUIControl没有新的属性,而且 UIView中的任何变化都不相关。

82761 次浏览

从 Xcode 11 beta 3开始

现在 UISegmentedControl上有 selectedSegmentTintColor属性。

参见 Rmaddy 的回答


回到 iOS12的样子

我不能调色的颜色的选定部分,希望它将在即将到来的测试版修复。

如果不设置正常状态的背景图像(这会移除所有 iOS 13样式) ,设置选定状态的背景图像就无法工作

但是我能够让它回到 iOS12的外观(或者接近足够,我不能返回它的角半径较小的尺寸)。

这并不理想,但一个明亮的白色分段控件看起来有点不适合我们的应用程序。

(没有意识到 UIImage(color:)在我们的代码库中是一种扩展方法,但是实现它的代码在网络上随处可见)

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

我已经尝试了这个变通方法,它对我很有用:

@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

从 Xcode 11 beta 3开始

现在 UISegmentedControl上有 selectedSegmentTintColor属性。

谢谢你@rmaddy!


原始答案,Xcode 11 beta 和 beta 2

有没有一种使用公共 API 而不需要深入挖掘私有子视图结构的适当解决方案?

在 Xcode 11.0 beta 测试版中,按照规则来做似乎是一个挑战,因为它基本上需要自己重绘每个州的所有背景图像,包括圆角、透明度和 resizableImage(withCapInsets:)。例如,您需要生成一个类似于下面这样的彩色图像:
enter image description here

因此,目前看来,让我们深入挖掘视频的方式似乎要容易得多:

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
}
}
}
}
}

此解决方案将正确地将色彩应用于所选内容,如下所示: enter image description here

以下是我对乔纳森的看法。的答案是 Xamarin.iOS (C #) ,但对图像大小进行了修复。就像 C ur 对 Colin Blake 的回答的评论一样,除了分割器,我把所有的图像都做成了分段控件的大小。分隔符是该段的1 x 高度。

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;
}

从 iOS13b3开始,UISegmentedControl上就有了 selectedSegmentTintColor

若要更改分段控件的整体颜色,请使用其 backgroundColor

要更改所选段的颜色,请使用 selectedSegmentTintColor

要更改未选定片段标题的颜色/字体,请使用状态为 .normal/UIControlStateNormalsetTitleTextAttributes

要更改所选段标题的颜色/字体,请使用状态为 .selected/UIControlStateSelectedsetTitleTextAttributes

如果使用图像创建分段控件,如果将图像创建为模板图像,则将使用分段控件的 tintColor为图像着色。但这有个问题。如果你将 tintColor设置为与 selectedSegmentTintColor相同的颜色,那么图像将不会在选定的片段中可见。如果将 tintColor设置为与 backgroundColor相同的颜色,那么未选区段上的图像将不可见。这意味着您的分段控件与图像必须使用3种不同的颜色,一切都是可见的。或者您可以使用非模板图像而不设置 tintColor

在 iOS12或更早版本中,只需设置分段控件的 tintColor或依赖于应用程序的整体色彩颜色。

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]];}

可以实现以下方法

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

用法代码

segmentControl.unselectedSegmentTintColor(.white)
segmentControl.selectedSegmentTintColor(.black)

返回文章页面@Ilahi Charfeddine 快速回答:

if #available(iOS 13.0, *) {
segmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.white], for: .selected)
segmentedControl.selectedSegmentTintColor = UIColor.blue
} else {
segmentedControl.tintColor = UIColor.blue
}

IOS13 UISegmentController

使用方法:

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()!)!
}
}

XCODE 11.1 & iOS 13

基于@Jigar Darji 的回答,但是一个更安全的实现。

我们首先创建一个方便失败的初始化程序:

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)
}
}

然后我们扩展 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
}
}
}

IOS 13和 Swift 5.0(Xcode 11.0)段控制100% 工作

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
}

虽然上面的答案很棒,但是大多数答案在选定的段落中得到的文本颜色是错误的。我已经创建了 UISegmentedControl子类,您可以在 iOS13和 iOS13之前的设备上使用它,并像在 iOS13之前的设备上一样使用 tintColor 属性。

    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)
}
}

使用 tintColorDidChange方法,我们可以确保每次段视图或任何底层视图的 tintColor属性发生变化时都会调用 stylize方法,这是 iOS 上的首选行为。

结果: enter image description here

如果你想设置背景清除,你必须这样做:

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)
}

结果是: enter image description here

SwiftUI Picker缺少一些基本选项。对于试图在 iOS13或14的 SwiftUI 中使用 SegmentedPickerStyle ()定制 Picker 的用户,最简单的选择是使用 UISegmentedControl.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)
}

但是,如果需要具有不同设置的多个控件,则使用 UISegmentedControl.appearance()进行全局外观设置选项不太好。另一种选择是为 UISegmentedControl实现 UIViewRepresentable。下面的示例设置了原始问题中询问的属性,并将 .apportionsSegmentWidthsByContent = true设置为奖励。希望这能帮你节省点时间。

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
}
}

IOS12 +

我为背景颜色费了很大劲。设定。清晰的颜色作为背景颜色总是添加默认的灰色。我是这么解决的

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


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

分色器的颜色

self.yourSegmentControl.setDividerImage(UIImage(), forLeftSegmentState: .normal, rightSegmentState: .normal, barMetrics: .default) // This will remove the divider image.

在 iOS13及以上版本中,当我在 viewDidLoad中访问分段控制的 subviews时,它有1个 UIImageView。一旦我插入更多的片段,它分别增加,所以3个片段意味着3个 UIImageView作为分段控制的 subviews

有趣的是,当它到达 viewDidAppear时,分段对照的 subviews变成了3个 UISegment(每个包含一个 UISegmentLabel作为它的子视图; 这是你的文本)和4个 UIImageView,如下所示:

viewDidAppear

这3个 UIImageViewviewDidLoad以某种方式成为 UISegment,我们不想触摸他们(但你可以尝试设置他们的图像或 isHidden,只是看看它如何影响的用户界面)。让我们忽略这些私人课程。

这4个 UIImageView实际上是3个“正常”的 UIImageView(连同1个 UIImageView子视图作为垂直分隔符)和1个“选中”的 UIImageView(也就是说,这实际上是你的 selectedSegmentTintColor图像,注意在上面的截图下面没有子视图)。

在我的例子中,我需要一个白色的背景,所以我必须隐藏灰色的背景图像(见: https://medium.com/flawless-app-stories/ios-13-uisegmentedcontrol-3-important-changes-d3a94fdd6763)。我还想删除/隐藏段之间的垂直分隔符。

因此,在 viewDidAppear中的简单解决方案,不需要设置分割器图像或背景图像(在我的例子中) ,就是简单地隐藏前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
}
}

或者

// 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
}
}
}

选择你的毒药..。

IOS13 + 解决方案中的灰色背景

自 iOS13以来,引入了更新的 UISegmentedControl。不幸的是,没有简单的方法来设置背景白色,因为框架正在添加半透明的灰色背景。结果,UISegmentedControl 的背景仍然是灰色的。有一个解决办法(丑陋,但有效) :

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