web-dev-qa-db-fra.com

Comment changer les couleurs d'un segment dans un UISegmentedControl dans iOS 13?

Un UISegmentedControl a une nouvelle apparence dans iOS 13 et le code existant pour modifier les couleurs du contrôle segmenté ne fonctionne plus comme ils le faisaient.

Avant iOS 13, vous pouviez définir le tintColor et celui-ci serait utilisé pour la bordure autour du contrôle segmenté, les lignes entre les segments et la couleur d'arrière-plan du segment sélectionné. Ensuite, vous pouvez changer la couleur des titres de chaque segment en utilisant l'attribut de couleur de premier plan avec titleTextAttributes.

Sous iOS 13, le tintColor ne fait rien. Vous pouvez définir backgroundColor du contrôle segmenté pour modifier la couleur globale du contrôle segmenté. Mais je ne trouve aucun moyen de modifier la couleur utilisée comme arrière-plan du segment sélectionné. La définition des attributs de texte fonctionne toujours. J'ai même essayé de définir la couleur d'arrière-plan du titre, mais cela n'affecte que l'arrière-plan du titre, pas le reste de la couleur d'arrière-plan du segment sélectionné.

En bref, comment modifier la couleur d'arrière-plan du segment actuellement sélectionné d'un UISegmentedControl dans iOS 13? Existe-t-il une solution appropriée, utilisant des API publiques, qui ne nécessite pas de creuser dans la structure de sous-vue privée?

Il n'y a pas de nouvelles propriétés dans iOS 13 pour UISegmentedControl ou UIControl et aucune des modifications de UIView n'est pertinente.

95
rmaddy

Depuis iOS 13b3, il existe désormais un selectedSegmentTintColor sur UISegmentedControl.

Pour modifier la couleur globale du contrôle segmenté, utilisez son backgroundColor.

Pour modifier la couleur du segment sélectionné, utilisez selectedSegmentTintColor.

Pour changer la couleur/police des titres de segments non sélectionnés, utilisez setTitleTextAttributes avec un état de .normal/UIControlStateNormal.

Pour modifier la couleur/police des titres de segment sélectionnés, utilisez setTitleTextAttributes avec un état de .selected/UIControlStateSelected.

Si vous créez un contrôle segmenté avec des images, si les images sont créées en tant qu'images de modèle, le tintColor du contrôle segmenté sera utilisé pour colorer les images. Mais cela a un problème. Si vous définissez tintColor sur la même couleur que selectedSegmentTintColor, l'image ne sera pas visible dans le segment sélectionné. Si vous définissez tintColor sur la même couleur que backgroundColor, les images sur les segments non sélectionnés ne seront pas visibles. Cela signifie que votre contrôle segmenté avec des images doit utiliser 3 couleurs différentes pour que tout soit visible. Ou vous pouvez utiliser des images non modèles et ne pas définir le tintColor.

Sous iOS 12 ou version antérieure, définissez simplement le tintColor du contrôle segmenté ou utilisez la couleur de teinte globale de l'application.

117
rmaddy

À partir de Xcode 11 beta 3

Il y a maintenant la propriété selectedSegmentTintColor sur UISegmentedControl.

Voir réponse de rmaddy


Pour retrouver l'apparence d'iOS 12

Je n'ai pas pu teinter la couleur du segment sélectionné, j'espère qu'il sera corrigé dans une prochaine version bêta.

La définition de l'image d'arrière-plan de l'état sélectionné ne fonctionne pas sans la définition de l'image d'arrière-plan de l'état normal (ce qui supprime tout le style iOS 13)

Mais j'ai pu le ramener à l'apparence iOS 12 (ou assez près, je n'ai pas pu ramener le rayon du coin à sa plus petite taille).

Ce n'est pas idéal, mais un contrôle segmenté blanc brillant semble un peu à sa place dans notre application.

(Je ne savais pas que UIImage(color:) était une méthode d'extension dans notre base de code. Mais le code pour l'implémenter est sur le 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

41
Jonathan.

IOS 13 et Swift 5.0 (Xcode 11.0) Contrôle de segment 100% de travail

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
}
17
Maulik Patel

À partir de Xcode 11 beta 3

Il y a maintenant la propriété selectedSegmentTintColor sur UISegmentedControl.

Merci @rmaddy!


Réponse originale, pour Xcode 11 beta et beta 2

Existe-t-il une solution appropriée, utilisant des API publiques, qui ne nécessite pas de creuser dans la structure de sous-vue privée?

Avec Xcode 11.0 beta, cela semble être un défi de le faire selon les règles, car il nécessite essentiellement de redessiner vous-même toutes les images d'arrière-plan pour chaque état, avec des coins arrondis, de la transparence et resizableImage(withCapInsets:). Par exemple, vous devrez générer une image colorée similaire à:
enter image description here

Donc pour l'instant, la façon de creuser dans les sous-vues semble beaucoup plus facile:

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

Cette solution appliquera correctement la couleur de teinte à la sélection, comme dans: enter image description here

10
Cœur
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
Ilahi Charfeddine

J'ai essayé la solution de contournement et cela fonctionne très bien pour moi. Voici la version Objective-C:

@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
8
Colin Blake

Version rapide de @Ilahi Charfeddine réponse:

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

Voici mon point de vue sur la réponse de Jonathan. Pour Xamarin.iOS (C #), mais avec des correctifs pour le dimensionnement de l'image. Comme pour le commentaire de Cœur sur la réponse de Colin Blake, j'ai fait toutes les images sauf le diviseur de la taille du contrôle segmenté. Le diviseur est 1xheight du 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;
}
4
t9mike

Vous pouvez implémenter la méthode suivante

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

Code d'utilisation

segmentControl.unselectedSegmentTintColor(.white)
segmentControl.selectedSegmentTintColor(.black)
3
Zain Anjum

iOS13 UISegmentController

comment utiliser:

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

XCODE 11.1 et iOS 1

Basé sur la réponse de @Jigar Darji mais une mise en œuvre plus sûre.

Nous créons d'abord un initialiseur pratique disponible:

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

Ensuite, nous étendons 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
    }
  }
}
1
FredFlinstone