可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答1:
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.
回答2:
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
}
}
}
回答3:
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
}
回答4:
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:
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:
回答5:
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
回答6:
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]];}
回答7:
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
}
回答8:
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;
}
回答9:
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)
回答10:
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()!)!
}
}
回答11:
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
}
}
}
回答12:
Expanding a little bit on Jonathan answer in case you don't want round corners
extension UISegmentedControl {
/// Tint color doesn't have any effect on iOS 13.
func ensureiOS12Style(roundCorner: Bool = true) {
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)
if !roundCorner {
layer.masksToBounds = false
let borderView = UIView()
borderView.layer.borderWidth = 1
borderView.layer.borderColor = UIColor.black.cgColor
borderView.isUserInteractionEnabled = false
borderView.translatesAutoresizingMaskIntoConstraints = false
addSubview(borderView)
NSLayoutConstraint(item: borderView, attribute: .centerX, relatedBy: .equal, toItem: self, attribute: .centerX, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: borderView, attribute: .centerY, relatedBy: .equal, toItem: self, attribute: .centerY, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: borderView, attribute: .width, relatedBy: .equal, toItem: self, attribute: .width, multiplier: 1, constant: 0).isActive = true
NSLayoutConstraint(item: borderView, attribute: .height, relatedBy: .equal, toItem: self, attribute: .height, multiplier: 1, constant: 0).isActive = true
}
}
}
}