I am trying to set UIsegmentedControl tint color for the selected segment to gradient color and I am unable to do it
I am trying to follow this article https://www.bethedev.com/2019/02/set-gradient-tint-color-for-segmented.html
Trying to use this code:
segmentedControl.setTitleTextAttributes([NSAttributedString.Key.foregroundColor:UIColor.white],for: UIControl.State.normal)
segmentedControl.setTitleTextAttributes([NSAttributedString.Key.foregroundColor:UIColor.white],for: UIControl.State.selected)
fileprivate func updateGradientBackground() {
let sortedViews = segmentedControl.subviews.sorted( by: { $0.frame.origin.x < $1.frame.origin.x } )
for (_, view) in sortedViews.enumerated() {
// let gradientImage = gradient(size: segmentedControl.frame.size, color: [UIColor.cyan,UIColor.blue])!
view.backgroundColor = UIColor(patternImage: UIImage(named: "segmentedRectangle.png")!)
view.tintColor = UIColor.clear
}
}
I am expecting only one segment to be of the segmentedRectangle.png image color but it is displaying on the entire segmented control like this.
Try this code, I put comments on relevant parts. Let me know if you need more explanation.
let segmentedControl: UISegmentedControl = {
let view = UISegmentedControl(items: ["Pounds", "Kilograms"])
view.selectedSegmentIndex = 0
view.tintColor = .black
view.backgroundColor = .white
view.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width - 40, height: 20)
/// Gradient
let gradient = CAGradientLayer()
gradient.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width - 40, height: 20)
let leftColor = UIColor.red
let rightColor = UIColor.purple
gradient.colors = [leftColor.cgColor, rightColor.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
/// Create gradient image
UIGraphicsBeginImageContext(gradient.frame.size)
gradient.render(in: UIGraphicsGetCurrentContext()!)
let segmentedControlImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// Normal Image
let rect: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1)
UIGraphicsBeginImageContext(rect.size);
let context:CGContext = UIGraphicsGetCurrentContext()!;
context.setFillColor(UIColor.white.cgColor)
context.fill(rect)
let normalImage:UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
/// Set segmentedControl image
view.setBackgroundImage(normalImage, for: .normal, barMetrics: .default)
view.setBackgroundImage(segmentedControlImage, for: .selected, barMetrics: .default)
return view
}()
Usage:
On your ViewDidLoad set navigationItem title view as your segmented control like so:-
self.navigationItem.titleView = segmentedControl
I think with few modifications/custominization you can get want you want, Cheers :)
StoryBoard/InterfaceBuilder
Just call this inside your ViewDidLoad and pass your outlet name on the function call: -
func configureSegementedControl(segmentedControl: UISegmentedControl) {
segmentedControl.selectedSegmentIndex = 0
segmentedControl.tintColor = .black
segmentedControl.backgroundColor = .white
segmentedControl.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width - 40, height: 20)
/// Gradient
let gradient = CAGradientLayer()
gradient.frame = CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width - 40, height: 20)
let leftColor = UIColor.red
let rightColor = UIColor.purple
gradient.colors = [leftColor.cgColor, rightColor.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
/// Create gradient image
UIGraphicsBeginImageContext(gradient.frame.size)
gradient.render(in: UIGraphicsGetCurrentContext()!)
let segmentedControlImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// Normal Image
let rect: CGRect = CGRect(x: 0, y: 0, width: 1, height: 1)
UIGraphicsBeginImageContext(rect.size);
let context:CGContext = UIGraphicsGetCurrentContext()!;
context.setFillColor(UIColor.white.cgColor)
context.fill(rect)
let normalImage:UIImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
/// Set segmentedControl image
segmentedControl.setBackgroundImage(normalImage, for: .normal, barMetrics: .default)
segmentedControl.setBackgroundImage(segmentedControlImage, for: .selected, barMetrics: .default)
}
Related
I am trying to make a UIButton with rounded corners that has 2 colored shadows. Why is the red (and at this point also the blue "shadow" layer covering the button? How to get the shadows below the button canvas). I thought it was helping to insert sublayers instead of just adding them.
I have made a playground illustrating the issue
import UIKit
import PlaygroundSupport
This is the button I'm trying to implement
class PrimaryButton: UIButton {
required init(text: String = "Test 1", hasShadow: Bool = true) {
super.init(frame: .zero)
setTitle(text, for: .normal)
backgroundColor = UIColor.blue
layer.cornerRadius = 48 / 2
layer.masksToBounds = false
if hasShadow {
insertShadow()
}
}
fileprivate func insertShadow() {
let layer2 = CALayer(layer: layer), layer3 = CALayer(layer: layer)
layer2.applySketchShadow(color: UIColor.red, alpha: 0.5, x: 0, y: 15, blur: 35, spread: -10)
layer3.applySketchShadow(color: UIColor.blue, alpha: 0.5, x: 0, y: 10, blur: 21, spread: -9)
layer.insertSublayer(layer2, at: 0)
layer.insertSublayer(layer3, at: 0)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
layer.sublayers?.forEach { (sublayer) in
sublayer.shadowPath = UIBezierPath(rect: bounds).cgPath
}
}
}
This is an extension that helps adding the shadow from Sketch specification:
extension CALayer {
func applySketchShadow(
color: UIColor = .black,
alpha: Float = 0.5,
x: CGFloat = 0,
y: CGFloat = 2,
blur: CGFloat = 4,
spread: CGFloat = 0)
{
shadowColor = color.cgColor
shadowOpacity = alpha
shadowOffset = CGSize(width: x, height: y)
shadowRadius = blur / 2.0
if spread == 0 {
shadowPath = nil
} else {
let dx = -spread
let rect = bounds.insetBy(dx: dx, dy: dx)
shadowPath = UIBezierPath(rect: rect).cgPath
}
masksToBounds = false
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let button = PrimaryButton()
button.frame = CGRect(x: 150, y: 200, width: 200, height: 48)
view.addSubview(button)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
It seems legit to me. layer1 & layer2 are sublayers of the button layer.
You could add a third layer that will serve as a background. Here is an example based on your code:
class PrimaryButton: UIButton {
let layer1 = CALayer(), layer2 = CALayer(), layer3 = CALayer()
override func layoutSubviews() {
super.layoutSubviews()
layer1.backgroundColor = UIColor.blue.cgColor
layer1.cornerRadius = 48 / 2
[layer1, layer2, layer3].forEach {
$0.masksToBounds = false
$0.frame = layer.bounds
layer.insertSublayer($0, at: 0)
}
layer2.applySketchShadow(color: UIColor.red, alpha: 0.5, x: 0, y: 15, blur: 35, spread: -10)
layer3.applySketchShadow(color: UIColor.blue, alpha: 0.5, x: 0, y: 10, blur: 21, spread: -9)
}
}
Note that I put most of the code inside layoutSubviews because most of your methods use the actual bounds of the button.
Change your insertions to:
layer.insertSublayer(layer2, at: 1)
layer.insertSublayer(layer3, at: 2)
That should do it.
Another way is to add double buttons without change your class.
let button = PrimaryButton()
button.frame = CGRect(x: 150, y: 200, width: 200, height: 48)
button.backgroundColor = UIColor.clear
view.addSubview(button)
self.view = view
let button1 = PrimaryButton()
button1.frame = CGRect(x: 0, y: 0, width: 200, height: 48)
button.addSubview(button1)
button1.layer.sublayers?.forEach{$0.removeFromSuperlayer()}
I'm trying to customize my segement control like below image. So, far I was able to customize its text attributes and color. Only problem is with the border. As per the below image, if my first segment is selected the border should apply to first segment top, right and second segment's bottom. And if my second segment is selected it should be the reverse ie, second segment top, left and first segments bottom.
Segment Model Image
Things done so far
UISegmentedControl.appearance().setTitleTextAttributes([NSAttributedStringKey.foregroundColor: UIColor.blue], for: .selected)
UISegmentedControl.appearance().setTitleTextAttributes([NSAttributedStringKey.foregroundColor: UIColor.green], for: .normal)
You can do this by adding an extension to UISegmentedControl. Try this.
extension UISegmentedControl {
private func defaultConfiguration(font: UIFont = UIFont.boldSystemFont(ofSize: 12), color: UIColor = UIColor.gray) {
let defaultAttributes = [
NSAttributedStringKey.font.rawValue: font,
NSAttributedStringKey.foregroundColor.rawValue: color
]
setTitleTextAttributes(defaultAttributes, for: .normal)
}
private func selectedConfiguration(font: UIFont = UIFont.boldSystemFont(ofSize: 12), color: UIColor = UIColor.blue) {
let selectedAttributes = [
NSAttributedStringKey.font.rawValue: font,
NSAttributedStringKey.foregroundColor.rawValue: color
]
setTitleTextAttributes(selectedAttributes, for: .selected)
}
private func removeBorder(){
let backgroundImage = getColoredRectImageWith(color: UIColor.white.cgColor, andSize: CGSize(width: self.bounds.size.width, height: self.bounds.size.height), yOffset: 2)
let backgroundImage2 = getColoredRectImageWith(color: UIColor.lightGray.cgColor, andSize: CGSize(width: self.bounds.size.width, height: self.bounds.size.height))
self.setBackgroundImage(backgroundImage2, for: .normal, barMetrics: .default)
self.setBackgroundImage(backgroundImage, for: .selected, barMetrics: .default)
self.setBackgroundImage(backgroundImage, for: .highlighted, barMetrics: .default)
let deviderImage = getColoredRectImageWith(color: UIColor.gray.cgColor, andSize: CGSize(width: 1.0, height: self.bounds.size.height))
self.setDividerImage(deviderImage, forLeftSegmentState: .selected, rightSegmentState: .normal, barMetrics: .default)
defaultConfiguration( color: UIColor.green)
selectedConfiguration(color: UIColor.blue)
}
func addUnderlineForSelectedSegment(){
removeBorder()
let underlineWidth: CGFloat = self.bounds.size.width / CGFloat(self.numberOfSegments)
let underlineHeight: CGFloat = 1.0
let underlineXPosition = CGFloat(selectedSegmentIndex * Int(underlineWidth))
let underLineYPosition = self.bounds.size.height - 2.0
let underlineFrame = CGRect(x: underlineXPosition, y: underLineYPosition, width: underlineWidth, height: underlineHeight)
let topUnderline = UIView(frame: underlineFrame)
topUnderline.backgroundColor = UIColor.gray
topUnderline.tag = 1
topUnderline.frame.origin.y = self.frame.origin.y
self.addSubview(topUnderline)
let bottomUnderline = UIView(frame: underlineFrame)
bottomUnderline.backgroundColor = UIColor.gray
bottomUnderline.tag = 2
bottomUnderline.frame.origin.x = topUnderline.frame.maxX
self.addSubview(bottomUnderline)
}
func changeUnderlinePosition(){
guard let topUnderline = self.viewWithTag(1) else {return}
let topUnderlineFinalXPosition = (self.bounds.width / CGFloat(self.numberOfSegments)) * CGFloat(selectedSegmentIndex)
topUnderline.frame.origin.x = topUnderlineFinalXPosition
guard let bottomUnderline = self.viewWithTag(2) else {return}
let underlineFinalXPosition = (selectedSegmentIndex == 0) ? topUnderline.frame.maxX : self.frame.origin.x
bottomUnderline.frame.origin.x = underlineFinalXPosition
}
private func getColoredRectImageWith(color: CGColor, andSize size: CGSize,yOffset:CGFloat = 0, hOffset:CGFloat = 0) -> UIImage{
UIGraphicsBeginImageContextWithOptions(size, false, 0.0)
let graphicsContext = UIGraphicsGetCurrentContext()
graphicsContext?.setFillColor(color)
let rectangle = CGRect(x: 0.0, y: 0.0 + yOffset, width: size.width, height: size.height - hOffset)
graphicsContext?.fill(rectangle)
let rectangleImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return rectangleImage!
}
}
Usage
In viewDidLoad add
mySegmentControl.addUnderlineForSelectedSegment()
And in your segment control action use
#IBAction func mySegmentControl(_ sender: UISegmentedControl) {
mySegmentControl.changeUnderlinePosition()
}
I have tried to put circle button on right navigation bar of iOS but unfortunately When I use button background it doesn't round the image it shows square shape background image but When I remove image and put background colour it round the button with background colour.
Code that I tried:
let button = UIButton()
button.frame = CGRectMake(0, 0, 40, 40)
button.layer.cornerRadius = 0.5 * button.bounds.size.width
button.setImage(self.myPic, forState: .Normal)
let barButton = UIBarButtonItem()
barButton.customView = button
self.navigationItem.rightBarButtonItem = barButton
Try to use this code..
for rounded button with image -
let button = UIButton()
button.frame = CGRectMake(0, 0, 40, 40)
let color = UIColor(patternImage: UIImage(named: "btnImage")!)
button.backgroundColor = color
button.layer.cornerRadius = 0.5 * button.bounds.size.width
let barButton = UIBarButtonItem()
barButton.customView = button
self.navigationItem.rightBarButtonItem = barButton
With Actual image---
let button = UIButton()
button.frame = CGRectMake(0, 0, 40, 40)
let image = UIImage(named: "btnImage")!
UIGraphicsBeginImageContextWithOptions(button.frame.size, false, image.scale)
let rect = CGRectMake(0, 0, button.frame.size.width, button.frame.size.height)
UIBezierPath(roundedRect: rect, cornerRadius: rect.width/2).addClip()
image.drawInRect(rect)
let newImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
let color = UIColor(patternImage: newImage)
button.backgroundColor = color
button.layer.cornerRadius = 0.5 * button.bounds.size.width
let barButton = UIBarButtonItem()
barButton.customView = button
self.navigationItem.rightBarButtonItem = barButton
I made a solution for Swift 4, you need to resize the image and the frame too
let avatarSize: CGFloat = 30
let button = UIButton()
button.frame = CGRect(x: 0, y: 0, width: avatarSize, height: avatarSize)
button.setImage(UIImage(named: "avatar")?.resizeImage(avatarSize, opaque: false), for: .normal)
if let buttonImageView = button.imageView {
button.imageView?.layer.cornerRadius = buttonImageView.frame.size.width / 2
button.imageView?.clipsToBounds = true
button.imageView?.contentMode = .scaleAspectFit
}
Extension you need:
extension UIImage {
func resizeImage(_ dimension: CGFloat, opaque: Bool, contentMode:
UIViewContentMode = .scaleAspectFit) -> UIImage {
var width: CGFloat
var height: CGFloat
var newImage: UIImage
let size = self.size
let aspectRatio = size.width/size.height
switch contentMode {
case .scaleAspectFit:
if aspectRatio > 1 { // Landscape image
width = dimension
height = dimension / aspectRatio
} else { // Portrait image
height = dimension
width = dimension * aspectRatio
}
default:
fatalError("UIIMage.resizeToFit(): FATAL: Unimplemented ContentMode")
}
if #available(iOS 10.0, *) {
let renderFormat = UIGraphicsImageRendererFormat.default()
renderFormat.opaque = opaque
let renderer = UIGraphicsImageRenderer(size: CGSize(width: width, height: height), format: renderFormat)
newImage = renderer.image {
(context) in
self.draw(in: CGRect(x: 0, y: 0, width: width, height: height))
}
} else {
UIGraphicsBeginImageContextWithOptions(CGSize(width: width, height: height), opaque, 0)
self.draw(in: CGRect(x: 0, y: 0, width: width, height: height))
newImage = UIGraphicsGetImageFromCurrentImageContext()!
UIGraphicsEndImageContext()
}
return newImage
}
}
swift 2
set bar button in circle
func setProfileImageOnBarButton() {
let button = UIButton()
button.setImage(profileImage, forState: UIControlState.Normal)
button.addTarget(self, action:#selector(self.openUserProfile), forControlEvents: UIControlEvents.TouchUpInside)
button.frame = CGRectMake(0, 0, 36, 36)
button.layer.cornerRadius = CGRectGetWidth(button.frame) / 2
button.layer.masksToBounds = true
let barButton = UIBarButtonItem(customView: button)
self.navigationItem.rightBarButtonItem = barButton
}
Swift 5 - You can simply do that, and make sure you make clipstobounds to true
teacherImage.setImage(UIImage(named: "icon_profile"), for: .normal)
teacherImage.frame = CGRect(x: 0, y: 0, width: 40, height: 40)
teacherImage.addTarget(self, action: #selector(addPressed), for: .touchUpInside)
teacherImage.layer.cornerRadius = 0.5 * teacherImage.bounds.size.width
teacherImage.clipsToBounds = true
let rightNavBarButton = UIBarButtonItem(customView: teacherImage)
let currWidth = rightNavBarButton.customView?.widthAnchor.constraint(equalToConstant: 40)
currWidth?.isActive = true
let currHeight = rightNavBarButton.customView?.heightAnchor.constraint(equalToConstant: 40)
currHeight?.isActive = true
self.navigationItem.rightBarButtonItem = rightNavBarButton
It works for Objective-C. Have a try!
UIButton *avatarButton = [[UIButton alloc]initWithFrame:CGRectMake(0, 0, 40, 40)];
// redraw the image to fit new size
UIGraphicsBeginImageContextWithOptions(avatarButton.frame.size, NO, 0);
[[UIImage imageNamed:#"pikachu"] drawInRect:CGRectMake(0, 0, avatarButton.frame.size.width, avatarButton.frame.size.height)];
UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
UIColor *color = [UIColor colorWithPatternImage: resultImage];
avatarButton.backgroundColor = color;
avatarButton.layer.cornerRadius = 0.5 * avatarButton.bounds.size.width;
UIBarButtonItem *barButton = UIBarButtonItem.new;
barButton.customView = avatarButton;
self.navigationItem.rightBarButtonItem = barButton;
I am trying to render a Label in a CALayer and the background of the layer iconLayer is being placed on top of the Label.
func textToImage(drawText: NSString, inImage: UIImage, atPoint:CGPoint)->UIImage{
let textColor: UIColor = UIColor(red: 85/255, green: 150/255, blue: 230/255, alpha: 1)
let textFont: UIFont = UIFont(name: "WeatherIcons-Regular", size: 20)!
UIGraphicsBeginImageContext(inImage.size)
let layer = CALayer()
let iconLayer = CALayer()
layer.frame = CGRectMake(0, 0, inImage.size.width, inImage.size.height)
inImage.drawInRect(CGRectMake(0, 0, inImage.size.width, inImage.size.height))
let imageSubLayer = CALayer()
imageSubLayer.contents = inImage.CGImage
let rect: CGRect = CGRectMake(atPoint.x, atPoint.y, 30, 30)
iconLayer.frame = rect
iconLayer.cornerRadius = 15.0
iconLayer.backgroundColor = UIColor.whiteColor().CGColor
layer.renderInContext(UIGraphicsGetCurrentContext()!)
iconLayer.borderColor = textColor.CGColor
iconLayer.borderWidth = 1
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
label.text = drawText as String
label.font = textFont
label.textColor = textColor
label.layer.renderInContext(UIGraphicsGetCurrentContext()!)
iconLayer.contents = label
layer.addSublayer(iconLayer)
layer.addSublayer(imageSubLayer)
layer.renderInContext(UIGraphicsGetCurrentContext()!)
let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
Below is what the view looks like with the background.
Below is what the view looks like with the background removed.
It seems the icon is not getting added to the layer iconLayer except it just getting added to the context.
I ended up fixing it with the following code.
func textToImage(drawText: NSString, inImage: UIImage, atPoint:CGPoint)->UIImage{
let textColor: UIColor = UIColor(red: 85/255, green: 150/255, blue: 230/255, alpha: 1)
let textFont: UIFont = UIFont(name: "WeatherIcons-Regular", size: 20)!
UIGraphicsBeginImageContext(inImage.size)
let layer = CALayer()
let iconLayer = CALayer()
layer.frame = CGRectMake(0, 0, inImage.size.width, inImage.size.height)
inImage.drawInRect(CGRectMake(0, 0, inImage.size.width, inImage.size.height))
let imageSubLayer = CALayer()
imageSubLayer.contents = inImage.CGImage
let rect: CGRect = CGRectMake(atPoint.x, atPoint.y, 30, 30)
iconLayer.frame = rect
iconLayer.cornerRadius = 15.0
iconLayer.backgroundColor = UIColor.whiteColor().CGColor
layer.renderInContext(UIGraphicsGetCurrentContext()!)
iconLayer.borderColor = textColor.CGColor
iconLayer.borderWidth = 1
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 30, height: 30))
label.text = drawText as String
label.font = textFont
label.textColor = textColor
iconLayer.contents = label.layer
layer.addSublayer(iconLayer)
layer.addSublayer(label.layer)
layer.addSublayer(imageSubLayer)
layer.renderInContext(UIGraphicsGetCurrentContext()!)
let newImage: UIImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return newImage
}
I'm able to add a custom status bar (with gradient) to the UIView like this:
override func viewDidLoad() {
super.viewDidLoad()
var statusBar : UIView = UIView(frame: CGRect(x: 0, y: 0, width: 375, height: 20))
let statusBarGradient : CAGradientLayer = CAGradientLayer()
statusBarGradient.frame = statusBar.bounds
let cor1 = UIColor(red: 0.416, green: 0.604, blue: 0.796, alpha: 1.0)
let cor2 = UIColor.whiteColor()
let arrayColors = [cor1.CGColor, cor2.CGColor]
statusBarGradient.colors = arrayColors
view.layer.insertSublayer(statusBarGradient, atIndex:0)
}
I would also like to add the same gradient to the footer but I'm not having much luck.
Your code works if you add the statusBar as a subview of the view, but I would make it dynamically size the width to fit the width of the superview, so just do the same thing for the footer, but dynamically set the height to be whatever the height of your view is minus the desired height of your footer bar.
let statusBar = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 20))
let footer = UIView(frame: CGRect(x: 0, y: view.frame.height - 20.0, width: view.frame.width, height: 20))
let statusBarGradient = CAGradientLayer()
let footerGradient = CAGradientLayer()
statusBarGradient.frame = statusBar.bounds
let cor1 = UIColor(red: 0.416, green: 0.604, blue: 0.796, alpha: 1.0)
let cor2 = UIColor.blackColor()
let arrayColors = [cor1.CGColor, cor2.CGColor]
footerGradient.frame = footer.bounds
let arrayColorsFooter = [cor2.CGColor, cor1.CGColor]
statusBarGradient.colors = arrayColors
footerGradient.colors = arrayColorsFooter
statusBar.layer.insertSublayer(statusBarGradient, atIndex:0)
footer.layer.insertSublayer(footerGradient, atIndex:0)
view.addSubview(statusBar)
view.addSubview(footer)