Custom UILabel with gradient text colour always become black - swift

I created a custom UILabel to show grediant text but it always show the text as black...when i but the same code on a view controller it works!!
import UIKit
class GradientLabel: UILabel {
override func awakeFromNib() {
super.awakeFromNib()
let gredient = GradientView.init(frame: CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height))
gredient.bottomColor = #colorLiteral(red: 0.1764705926, green: 0.4980392158, blue: 0.7568627596, alpha: 1)
gredient.topColor = #colorLiteral(red: 0.1764705926, green: 0.01176470611, blue: 0.5607843399, alpha: 1)
setTextColorToGradient(image: imageWithView(view: gredient)!)
}
func imageWithView(view: UIView) -> UIImage? {//bet7awel uiview to image
UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
let img = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return img
}
func setTextColorToGradient(image: UIImage) {//beta7'od image we tekteb beha el text fe el label
UIGraphicsBeginImageContext(frame.size)
image.draw(in: bounds)
let myGradient = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
textColor = UIColor(patternImage: myGradient!)
}
}

You don’t want to rely on awakeFromNib. Furthermore, you really don’t want to do this in init, either, as you want to be able to respond to size changes (e.g. if you have constraints and the frame changes after the label is first created).
Instead, update your gradient from layoutSubviews, which is called whenever the view’s frame changes:
#IBDesignable
class GradientLabel: UILabel {
#IBInspectable var topColor: UIColor = #colorLiteral(red: 0.1764705926, green: 0.4980392158, blue: 0.7568627596, alpha: 1) {
didSet { setNeedsLayout() }
}
#IBInspectable var bottomColor: UIColor = #colorLiteral(red: 0.1764705926, green: 0.01176470611, blue: 0.5607843399, alpha: 1) {
didSet { setNeedsLayout() }
}
override func layoutSubviews() {
super.layoutSubviews()
updateTextColor()
}
private func updateTextColor() {
let image = UIGraphicsImageRenderer(bounds: bounds).image { _ in
let gradient = GradientView(frame: bounds)
gradient.topColor = topColor
gradient.bottomColor = bottomColor
gradient.drawHierarchy(in: bounds, afterScreenUpdates: true)
}
textColor = UIColor(patternImage: image)
}
}
That results in:
Note,
I’ve used the new UIGraphicsImageRenderer rather than UIGraphicsBeginImageContext, UIGraphicsGetImageFromCurrentImageContext, and UIGraphicsEndImageContext.
Rather than building a CGRect manually, I just use bounds.
I’ve made it #IBDesignable so that I can use it directly in Interface Builder. I’ve also made the colors #IBInspectable so you can adjust the colors right in IB rather than going to code. Clearly, you only have to do this if you want to see the gradient effect rendered in IB.
I’ve made it update the gradient (a) when the label needs to be laid out again; and (b) whenever you change either of the colors.
For what it’s worth, this is the GradientView I used for the purposes of this example:
#IBDesignable
class GradientView: UIView {
override class var layerClass: AnyClass { return CAGradientLayer.self }
private var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
#IBInspectable var topColor: UIColor = .white { didSet { updateColors() } }
#IBInspectable var bottomColor: UIColor = .blue { didSet { updateColors() } }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
updateColors()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
updateColors()
}
private func updateColors() {
gradientLayer.colors = [topColor.cgColor, bottomColor.cgColor]
}
}
In this case, because we’re setting the layerClass to the gradient, all we need to do is to configure it during init, and the base layerClass will take care of responding the size changes.
Alternatively, you can just draw your gradient using CoreGraphics:
#IBDesignable
class GradientLabel: UILabel {
#IBInspectable var topColor: UIColor = #colorLiteral(red: 0.1764705926, green: 0.4980392158, blue: 0.7568627596, alpha: 1) {
didSet { setNeedsLayout() }
}
#IBInspectable var bottomColor: UIColor = #colorLiteral(red: 0.1764705926, green: 0.01176470611, blue: 0.5607843399, alpha: 1) {
didSet { setNeedsLayout() }
}
override func layoutSubviews() {
super.layoutSubviews()
updateTextColor()
}
private func updateTextColor() {
let image = UIGraphicsImageRenderer(bounds: bounds).image { context in
let colors = [topColor.cgColor, bottomColor.cgColor]
guard let gradient = CGGradient(colorsSpace: nil, colors: colors as CFArray, locations: nil) else { return }
context.cgContext.drawLinearGradient(gradient,
start: CGPoint(x: bounds.midX, y: bounds.minY),
end: CGPoint(x: bounds.midX, y: bounds.maxY),
options: [])
}
textColor = UIColor(patternImage: image)
}
}
This achieves the same as the first example, but is potentially a tad more efficient.

You should init your label instead of awakeFromNib cause you are not using a xib for your label
class GradientLabel: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
}
and an extension would be perfect to make your code reusable
extension UILabel {
func setTextColorToGradient(_ image: UIImage) {
UIGraphicsBeginImageContext(frame.size)
image.draw(in: bounds)
let myGradient = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
textColor = UIColor(patternImage: myGradient!)
}
}
usage:
label.setTextColorToGradient(UIImage(named:"someImage")!)

Related

Displaying CAShapeLayer in a collection cell

Good afternoon. The problem with the display of CAShapeLayer layers, or rather with the transfer of information there.
Depending on the rating of the film, the color of the circle changes.
At the first load, everything is displayed correctly in the collection. But once you scroll the collection down (10 cells in total) and up again, the occupancy of the circle and the color change. Photo below:
I get the data from the API, pass it to the cell: image, genre, movie title and rating figure. Everything is transmitted correctly and correctly.
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "PreviewCardCollectionViewCell", for: indexPath) as! PreviewCardCollectionViewCell
cell.configure(name: movies[indexPath.row].title!, genres: movies_genres[indexPath.row][0], partOfPosterURL: movies[indexPath.row].posterPath!, voteAverage: movies[indexPath.row].voteAverage!)
return cell
}
In PreviewCardCollectionViewCell (here is not the whole code, but the one that concerns the problem):
class PreviewCardCollectionViewCell: UICollectionViewCell {
private lazy var circleView: UIView = {
var view = UIView()
view.backgroundColor = .black
view.layer.cornerRadius = chartViewSize/2
view.translatesAutoresizingMaskIntoConstraints = false
return view
}()
private lazy var chartView: CircleChartView = {
var chart = CircleChartView()
chart.translatesAutoresizingMaskIntoConstraints = false
return chart
}()
override init(frame: CGRect) {
super.init(frame: frame)
//circleView
self.addSubview(circleView)
circleView.heightAnchor.constraint(equalToConstant: chartViewSize).isActive = true
circleView.widthAnchor.constraint(equalToConstant: chartViewSize).isActive = true
circleView.bottomAnchor.constraint(equalTo: previewCardImageView.bottomAnchor, constant: chartViewSize/2).isActive = true
circleView.leadingAnchor.constraint(equalTo: previewCardImageView.leadingAnchor, constant: 10).isActive = true
//chartView
circleView.addSubview(chartView)
chartView.widthAnchor.constraint(equalToConstant: chartViewSize * 0.93).isActive = true
chartView.heightAnchor.constraint(equalToConstant: chartViewSize * 0.93).isActive = true
chartView.centerXAnchor.constraint(equalTo: circleView.centerXAnchor).isActive = true
chartView.centerYAnchor.constraint(equalTo: circleView.centerYAnchor).isActive = true
}
func configure(name: String, genres: String, partOfPosterURL: String, voteAverage: Double) {
//chartView
chartView.percentage = voteAverage
}
And already from the configure() configuration, the rating digit is passed to the UIView: CircleChartView class (here's the whole code).
class CircleChartView: UIView {
var circleLayer = CAShapeLayer()
var progressLayer = CAShapeLayer()
var percentage: Double = 0.0
private var startPoint = CGFloat(-Double.pi / 2)
private var endPoint = CGFloat(3 * Double.pi / 2)
private var halfSize: CGFloat {
get {
return min(bounds.size.width/2, bounds.size.height/2)
}
}
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
override func draw(_ rect: CGRect) {
drawCircle(CAlayer: circleLayer, draw: .baseCircle, ratio: 0.0)
drawCircle(CAlayer: progressLayer, draw: .fillProgressCircle, ratio: percentage)
}
private func drawCircle(CAlayer: CAShapeLayer, draw: CircleChartDraw, ratio: Double) {
var circularPath = UIBezierPath()
switch draw {
case .baseCircle:
circularPath = UIBezierPath(
arcCenter: CGPoint(x: halfSize, y: halfSize),
radius: CGFloat(halfSize - (4.0/2)),
startAngle: startPoint,
endAngle: endPoint,
clockwise: true)
case .fillProgressCircle:
circularPath = UIBezierPath(
arcCenter: CGPoint(x:halfSize,y:halfSize),
radius: CGFloat(halfSize - (4.0/2) ),
startAngle: startPoint,
endAngle: startPoint + CGFloat(Double.pi * 2 * ratio/10),
clockwise: true)
}
CAlayer.path = circularPath.cgPath
CAlayer.fillColor = UIColor.clear.cgColor
if self.percentage >= 7.0 && self.percentage <= 10.0 {
CAlayer.strokeColor = UIColor(red: 50/255, green: 205/255, blue: 50/255, alpha: draw.alphaColor).cgColor
} else if self.percentage >= 4.0 && self.percentage <= 6.9 {
CAlayer.strokeColor = UIColor(red: 255/255, green: 255/255, blue: 51/255, alpha: draw.alphaColor).cgColor
} else if self.percentage >= 0.0 && self.percentage <= 3.9 {
CAlayer.strokeColor = UIColor(red: 255/255, green: 69/255, blue: 0/255, alpha: draw.alphaColor).cgColor
}
CAlayer.lineWidth = 3.0
CAlayer.lineCap = .round
CAlayer.strokeEnd = 1.0
self.layer.addSublayer(CAlayer)
}
}
The problem is that the data is transmitted correctly, and then somehow magically they are mixed exactly in CircleChartView.
Hierarchy:
Below is the code of what data should come, for example, in the first cell. (Scrolling down/up was done several times, and the screen has a completely different filling of the circle and color):
if indexPath.row == 0 {
print("Row \(0): \(movies[0].voteAverage!)")
}
I found the problem. Moved everything to the func drawCircle()
func drawCircle() {
drawCircle(CAlayer: circleLayer, draw: .baseCircle, ratio: 0.0)
drawCircle(CAlayer: progressLayer, draw: .fillProgressCircle, ratio: percentage)
}
And I call her in UIView:
override func draw(_ rect: CGRect) {
drawCircle()
}
And in UICollectionViewCell:
override func prepareForReuse() {
super.prepareForReuse()
chartView.circleLayer.removeFromSuperlayer()
chartView.progressLayer.removeFromSuperlayer()
}
And in UIViewController:
cell.chartView.drawCircle()

Swift shadow does not spread correctly

I'm been trying to create a shadow for my UIView. I looked around and found an extension for the CALayer class from this post. https://stackoverflow.com/a/48489506/9188318
So far its been working well for me until I try to put in a non 0 number for the spread.
With the spread being 0. This is the result
And here is the result using a spread of 1
And it gets even worse with a spread of 5
The problem that I'm having is that it doesn't have rounded corners and I have no idea how to fix this. Heres my code for the UIView that uses this view. The extension that is used to make the shadow is in the post above.
UIView code
class FileCalculateOperatorSelectionButton : UIView {
private var isActivated : Bool = false
//Background colors
private var unSelectedBackgroundColor : UIColor = UIColor(red: 178/255, green: 90/255, blue: 253/255, alpha: 1.0)
private var selectedBackgroundColor : UIColor = UIColor.white
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.commonInit()
}
private func commonInit() {
self.layer.cornerRadius = self.frame.height/2
self.backgroundColor = self.unSelectedBackgroundColor
let shadowColor = UIColor(red: 0xC8, green: 0xC6, blue: 0xC6)
//Change the spread argument here
self.layer.applySketchShadow(color: .black, alpha: 0.5, x: 0, y: 0, blur: 5, spread: 0)
}
}
Try setting the layer's masksToBounds property to true.
self.layer.masksToBounds = true
Change shadowPath in applySketchShadow function in CALayer extension like below;
// shadowPath = UIBezierPath(rect: rect).cgPath
shadowPath = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius).cgPath
when spread: 1

Adding custom UIView to the view controller causes it to disappear in the storyboard

I created a custom UIView and then added it to a number of viewControllers. The view controller become glitchy and do not render in the storyboard. I just see the view controllers' outlines and no content. If I remove the custom view then all goes back to normal.
Can someone please take a look at my custom view code and see if I am doing anything wrong? Thank you so much.
import UIKit
#IBDesignable
class ProgressView: UIView {
#IBOutlet var contentView: UIView!
#IBInspectable var percentageProgress: CGFloat = 0.0 {
didSet {
self.setGradient()
}
}
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed("ProgressView", owner: self, options: nil)
addSubview(contentView)
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
setGradient()
}
private func setGradient() {
if let sublayers = self.contentView.layer.sublayers {
for sublayer in sublayers {
sublayer.removeFromSuperlayer()
}
}
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [UIColor(#colorLiteral(red: 0.3935321569, green: 0.3986310661, blue: 0.6819975972, alpha: 1)).cgColor, UIColor(#colorLiteral(red: 0.1705667078, green: 0.649338007, blue: 0.5616513491, alpha: 1)).cgColor]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
gradientLayer.cornerRadius = contentView.frame.height / 2
contentView.layer.cornerRadius = contentView.frame.height / 2
self.contentView.layer.insertSublayer(gradientLayer, at: 0)
gradientLayer.frame = CGRect(x: contentView.frame.minX, y: contentView.frame.minY,
width: self.contentView.frame.maxX * (percentageProgress ?? 1.0), height:
contentView.frame.maxY)
self.contentView.setNeedsDisplay()
}
}

Gradient Layer not showing on UIButton

I am trying to create a custom UIButton that i can use throughout my application. I would like to apply a gradient as its background however the gradient is not showing up using this very simple Code.
It would be very helpful if someone could point out my mistake in the code below.
class GradientBtn: UIButton {
let gradientLayer = CAGradientLayer()
override init(frame: CGRect) {
super.init(frame: frame)
themeConfig()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
themeConfig()
}
private func themeConfig() {
//shadow
layer.shadowOffset = CGSize.zero
layer.shadowColor = UIColor.gray.cgColor
layer.shadowOpacity = 1.0
//titletext
setTitleColor(Theme.colorWhite, for: .normal)
titleLabel?.font = UIFont(name: Theme.fontAvenir, size: 18)
//rounded corners
layer.cornerRadius = frame.size.height / 2
//gradient
gradientLayer.locations = [0.0, 1.0]
gradientLayer.colors = [Theme.colorlightBlue, Theme.colorMidBlue]
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.0)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0)
gradientLayer.frame = bounds
layer.insertSublayer(gradientLayer, at: 0)
}
}
Replace
gradientLayer.colors = [Theme.colorlightBlue, Theme.colorMidBlue]
with
gradientLayer.colors = [Theme.colorlightBlue.cgColor, Theme.colorMidBlue.cgColor]
And it's always good to add
override func layoutSubviews() {
super.layoutSubviews()
gradientLayer.frame = self.bounds
}
Sh_Khan is right about the CGColor references, but a few additional refinements:
Rather than adding the gradient as a sublayer, make it the layerClass of the UIButton. This makes animations much better (e.g. if the button changes size on device rotation).
When you do this, remove all attempts to set the frame of the gradientLayer.
In layoutSubviews, you’ll no longer need to update the gradientLayer.frame, so remove that entirely, but you do want to put your corner rounding there.
It’s extremely unlikely to ever matter, but when you set layer.cornerRadius (or any property of the view, its layer, or that layer's sublayers), you should never refer to self.frame. The frame of the view is the CGRect in the superview’s coordinate system. Only refer to the view’s bounds within the subclass; never its frame.
Thus:
class GradientBtn: UIButton {
override class var layerClass: AnyClass { return CAGradientLayer.self }
private var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
themeConfig()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
themeConfig()
}
private func themeConfig() {
//shadow
layer.shadowOffset = .zero
layer.shadowColor = UIColor.gray.cgColor
layer.shadowOpacity = 1
//titletext
setTitleColor(Theme.colorWhite, for: .normal)
titleLabel?.font = UIFont(name: Theme.fontAvenir, size: 18)
//gradient
gradientLayer.locations = [0, 1]
gradientLayer.colors = [Theme.colorlightBlue, Theme.colorMidBlue].map { $0.cgColor }
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
}
override func layoutSubviews() {
super.layoutSubviews()
layer.cornerRadius = min(bounds.height, bounds.width) / 2
}
}

my drawRect function will not update

I want the values for the colors of the circle to update when the variables(redd1,greenn1,bluee1) are changed by the steppers in my ViewController. The original circle is drawn by drawRect.
var redd1 = 0.0;
var greenn1 = 0.0;
var bluee1 = 0.0;
override init(frame: CGRect)
{
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
}
override func drawRect(rect: CGRect)
{
let circle2 = UIView(frame: CGRect(x: -25.0, y: 10.0, width: 100.0, height:100.0))
circle2.layer.cornerRadius = 50.0
let startingColor2 = UIColor(red: (CGFloat(redd1))/255, green: (CGFloat (greenn1))/255, blue: (CGFloat(bluee1))/255, alpha: 1.0)
circle2.backgroundColor = startingColor2;
addSubview(circle2);
}
I tried creating a new function that draws a new circle on top of the old one. It is called by the stepper. The new function, updateColor(), receives the new value from the stepper. This partially works because it prints out the correct new values, but never draws the new circle.
func updateColor()
{
let circle = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0))
circle.layer.cornerRadius = 50.0;
let startingColor = UIColor(red: (CGFloat(redd1))/255, green: (CGFloat(greenn1))/255, blue: (CGFloat(bluee1))/255, alpha: 1.0)
circle.backgroundColor = startingColor;
addSubview(circle);
}
I'm not so good in Swift, but that's a simple way how i wood do it:
var circle2: UIView
override func drawRect(rect: CGRect) {
circle2 = UIView(frame: CGRect(x: -25.0, y: 10.0, width: 100.0, height:100.0))
circle2.layer.cornerRadius = 50.0
let startingColor2 = UIColor(red: (CGFloat(redd1))/255, green: (CGFloat (greenn1))/255, blue: (CGFloat(bluee1))/255, alpha: 1.0)
circle2.backgroundColor = startingColor2;
addSubview(circle2);
}
#IBAction valueChanged(sender: UIStepper) {
changeColor(color(the Color you want), Int(the Int value you want the color to change at))
}
func changeColor(color: UIColor, number: Int) {
if stepper.value == number {
circle2.backgroundColor = color
}
}
You have to create a IBAction for the stepper of the type value changed and call the method for every color and to it belonging Int in the IBAction for the stepper. So every time + or - on the stepper is pressed the IBAction calls the changeColor method and the changeColor method tests which color should be set to circle2.