How to change the colors of a segment in a UISegmentedControl in iOS 13?

前端 未结 14 738
醉话见心
醉话见心 2020-11-30 18:13

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

相关标签:
14条回答
  • 2020-11-30 18:20

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

    0 讨论(0)
  • 2020-11-30 18:23

    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
    }
    
    0 讨论(0)
  • 2020-11-30 18:27

    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:

    0 讨论(0)
  • 2020-11-30 18:34

    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;
    }
    
    0 讨论(0)
  • 2020-11-30 18:34

    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)
    
    0 讨论(0)
  • 2020-11-30 18:35

    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:

    0 讨论(0)
提交回复
热议问题