Gradient tint color in segmented control
Asked Answered
F

2

6

I get gradient image with this method

func gradient(size:CGSize,color:[UIColor]) -> UIImage?{
    //turn color into cgcolor
    let colors = color.map{$0.cgColor}
    //begin graphics context
    UIGraphicsBeginImageContextWithOptions(size, true, 0.0)
    guard let context = UIGraphicsGetCurrentContext() else {
        return nil
    }
    // From now on, the context gets ended if any return happens
    defer {UIGraphicsEndImageContext()}
    //create core graphics context
    let locations:[CGFloat] = [0.0,1.0]
    guard let gredient = CGGradient.init(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: colors as NSArray as CFArray, locations: locations) else {
        return nil
    }
    //draw the gradient
    context.drawLinearGradient(gredient, start: CGPoint(x:0.0,y:size.height), end: CGPoint(x:size.width,y:size.height), options: [])
    // Generate the image (the defer takes care of closing the context)
    return UIGraphicsGetImageFromCurrentImageContext()
}

Then I set tintColor of segmented control to gradient:

    let gradientImage = gradient(size: listSegmentedControl.frame.size, color: [UIColor.black, UIColor.red])!
    listSegmentedControl.tintColor = UIColor(patternImage: gradientImage)

and that doesn't work. However, same code works for setting backgroundColor:

    let gradientImage = gradient(size: listSegmentedControl.frame.size, color: [UIColor.black, UIColor.red])!
    listSegmentedControl.backgroundColor = UIColor(patternImage: gradientImage)

Does anybody have any ideas why? I really need to set gradient tintColor. Any help is very appreciated.

EDIT:

Ideally I want my segmented control to look like this:

enter image description here

Fester answered 2/7, 2018 at 17:15 Comment(2)
This might help: #34734443Powell
@Dopapp thanks! I'll give it a tryFester
C
5

This is a known hack to change the tint color of UISegmentedControl

   let sortedViews = listSegmentedControl.subviews.sorted( by: { $0.frame.origin.x < $1.frame.origin.x } )

    for (index, view) in sortedViews.enumerated() {
        if index == listSegmentedControl.selectedSegmentIndex {
            view.tintColor = UIColor(patternImage: gradientImage)
        } else {
            view.tintColor = UIColor.gray //Whatever the color of non selected segment controller tab
        }
    }

Though looks like a ugly hack, I have been using it from a quite a while and seems fairly straight forward. Hope it helps.

EDIT:

Is this what you need buddy?

enter image description here

If yes lemme know, Ill post the code for the same.

EDIT 2:

As OP has mentioned in his comment that the output he is expecting is same as the one I showed in image above, providing code for the same.

Disclaimer:

As mentioned by rmaddy in his comments below, this is a hack and makes use of undocumented (Complete public API though) but a very well known hack to change the tint color of UISegemntedControl that exists from as far as iOS 5 (Thats how I remember, lemme know if I am wrong )

So use answer with the caution in mind that, in future releases of iOS Apple might change the structure of subviews in UISegemntedControl and might affect your O/P. Nothing that I can see, will result in crash but might affect the way O/P is rendered on screen.

I have declared a variable so that GradientImage can be generated only once, but its up to your implementation to use it the way you want

var gradientImage : UIImage! = nil

In ViewDidLoad I initialize the gradientImage and UISegmentedControl as

override func viewDidLoad() {
        super.viewDidLoad()
        gradientImage = gradient(size: segmentControl.frame.size, color: [UIColor.black, UIColor.red])!

        //I have specified custom font need not necessarily be used
        //Font color attribute is important though, usually `UISegementedControl` title takes color from tint color, because we might need a different color for text to highlight above gradient color am using custom font colors

        let font = UIFont(name: "HelveticaNeue-Medium", size: 20)
        segmentControl.setTitleTextAttributes([NSFontAttributeName : font!, NSForegroundColorAttributeName : UIColor.blue], for: .normal)
        segmentControl.setTitleTextAttributes([NSForegroundColorAttributeName : UIColor.white], for: .selected)

        //Set the border color and border to `UISegmentedControl` and also make it round corner

        segmentControl.layer.borderColor = UIColor(patternImage: gradientImage).cgColor
        segmentControl.layer.borderWidth = 2
        segmentControl.layer.masksToBounds = true
        segmentControl.layer.cornerRadius = 10

        //In order to update the selected Segment tint and background color we need to call multiple statements every time selection changes hence I have moved it to the function and called it in viewDidLoad

        updateGradientBackground()
    }

Finally updateGradientBackground function definition is same as the one I posted in my original answer

fileprivate func updateGradientBackground() {
        let sortedViews = segmentControl.subviews.sorted( by: { $0.frame.origin.x < $1.frame.origin.x } )
        for (index, view) in sortedViews.enumerated() {
            if index == segmentControl.selectedSegmentIndex {
                //very important thing to notice here is because tint color was not honoring the `UIColor(patternImage` I rather used `backgroundColor` to create the effect and set clear color as clear color
                view.backgroundColor = UIColor(patternImage: self.gradientImage)
                view.tintColor = UIColor.clear
            } else {
                //very important thing to notice here is because tint color was not honoring the `UIColor(patternImage` I rather used `backgroundColor` to create the effect and set clear color as clear color
                view.backgroundColor = UIColor.white //Whatever the color of non selected segment controller tab
                view.tintColor = UIColor.clear
            }
        }
    }

Finally, in IBAction of UISegmentedControl, simply call

@IBAction func segmentControllerTapped(_ sender: UISegmentedControl) {
    self.updateGradientBackground()
}

Hope this helps

Cowpea answered 2/7, 2018 at 17:36 Comment(13)
Please note that this code depends on the undocumented and private subview structure of UISegmentedControl. This code could fail with any iOS update that changes that private subview structure. At least is shouldn't crash.Persson
@Persson : Agreed. Hence I mentioned it as a ugly hack. A hack that works for now but runs with a potential danger of breaking in future, that being said this hack seems to be working fine from iOS5-6 till now. Not mentioning anything in defense of my answer :)Cowpea
Thanks for your answer! I implemented your code in my app and it doesn't work. I must be doing something wrong.. Can you please check it from your side and let me know if it works for you?Fester
@lavrin-pristazh : Checking nowCowpea
@lavrin-pristazh : But dude the gradient you provided does not have same color effect, with the gradient you provided out put you get is what I posted aboveCowpea
Yeah, sorry, the gradient I provided was just for testing purposes. My problem is that I cannot get that frame around unselected segment, and corners are not rounded.Fester
@lavrin-pristazh : Take a look at the updated image, dont worry about the squared corner, u can make it rounded :)Cowpea
Yeah, that's want I need. If you shared your code you'd help me a lot and I'll accept your answerFester
@lavrin-pristazh : gimme a minute updating answerCowpea
@lavrin-pristazh : Updated my answer, read the comments carefullyCowpea
@lavrin-pristazh : Thanks a lot buddy :) Happy coding :)Cowpea
I just changed your code a little and set up my segmented control exactly how I wanted. Thank you so much, you saved my day :)Fester
@lavrin-pristazh : Am glad I could be of help :) Happy coding :)Cowpea
E
0

Implemented gradient color for knob on iOS13+ UISegmentControl. Added subview with gradient then set mask to it and copying animations to match original knob.

// 1 - Make subclass
class GradientSegment: UISegmentedControl {
    // this could be UIImageView
    lazy var gradientView: GradientView = {
        let view = GradientView(frame: self.bounds)
        view.startColor = ...
        view.endColor = ...
        view.horizontalMode = true
        view.autoresizingMask = [.flexibleHeight, .flexibleWidth]
        return view
    }()

    let gradientMask = UIImageView()

    // 2 - override init
    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setup()
    }

    private func setup() {
        addSubview(gradientView)
        gradientView.mask = gradientMask
    }

    // 3 - Copy knob position and animations
    override func insertSubview(_ view: UIView, at index: Int) {
        super.insertSubview(view, at: index)
        if index == 3, let view = view as? UIImageView {
            gradientMask.image = view.image
            gradientMask.frame = view.frame

            if let keys = view.layer.animationKeys() {
                for key in keys {
                    guard let animation = view.layer.animation(forKey: key) else {
                        continue
                    }
                    gradientMask.layer.add(animation, forKey: key)
                }
            }
        }
    }
}

This is not safe approach, because appearance will become invalid when apple will change control layout, but for now it is working.

Eddington answered 7/4, 2021 at 17:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.