Adding gradient to UILabel through extension - ios

as you probably know, if you try to set a gradient background to an UILabel it overwrites the text.
I would like to write an extension to UILabels in order to put a gradient background.
This is my code, but the gradient is put in a square moved down and right to the label. I cannot understand why, since I am using the same frame
public extension UILabel {
func applyGradientToLabelAtAngle(_ angle : CGFloat, _ color1 : UIColor, _ color2 : UIColor){
let gradient = CAGradientLayer()
gradient.frame = self.frame
gradient.colors = [color1.cgColor, color2.cgColor]
gradient.calculatePoints(for: angle)
let index = self.layer.superlayer!.sublayers!.firstIndex(of: self.layer)!
self.layer.superlayer!.sublayers!.insert(gradient, at: index)
}
}
edit: calculatePoints function calculate the locations, not important for the problem

Insufficient information. Your code works fine for me when I call it like this:
let lab = UILabel()
lab.text = "Howdy there"
lab.sizeToFit()
lab.frame.origin = .init(x:100, y:100)
self.view.addSubview(lab)
lab.applyGradientToLabelAtAngle(.pi, .green, .red)
The problem is evidently that you not calling it like that. But you didn't say how you are calling it. I suspect that you are calling it at the wrong time, namely at a time when the all-important self.frame is not yet known.

Related

Swift ios cut out rounded rect from view allowing colour changes

I'm using this approach to cut out a rounded rect "window" from a background view:
override func draw(_ rect: CGRect) {
guard let rectsArray = rectsArray else {
return
}
for holeRect in rectsArray {
let holeRectIntersection = rect.intersection(holeRect)
if let context = UIGraphicsGetCurrentContext() {
let roundedWindow = UIBezierPath(roundedRect: holeRect, cornerRadius: 15.0)
if holeRectIntersection.intersects(rect) {
context.addPath(roundedWindow.cgPath)
context.clip()
context.clear(holeRectIntersection)
context.setFillColor(UIColor.clear.cgColor)
context.fill(holeRectIntersection)
}
}
}
}
In layoutSubviews() I update the background colour add my "window frame" rect:
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = self.baseMoodColour
isOpaque = false
self.rectsArray?.removeAll()
self.rectsArray = [dragAreaView.frame]
}
I'm adding the rect here because layoutSubviews() updates the size of the "window frame" (i.e., the rect changes after layoutSubviews() runs).
The basic mechanism works as expected, however, if I change the background colour, the cutout window fills with black. So I'm wondering how I can animate a background colour change with this kind of setup? That is, I want to animate the colour of the area outside the cutout window (the window remains clear).
I've tried updating backgroundColor directly, and also using didSet in the accessor of a custom colour variable in my UIView subclass, but both cause the same filling-in of the "window".
var baseMoodColour: UIColor {
didSet {
self.backgroundColor = baseMoodColour
self.setNeedsDisplay()
}
}
Try to use UIView.animate, you can check it here
UIView.animate(withDuration: 1.0, delay: 0.0, options: [.curveEaseOut], animations: {
self.backgroundColor = someNewColour
//Generally
//myView.backgroundColor = someNewColor
}, nil)
The problem in the short run is that that is simply what clear does if the background color is opaque. Just give your background color some transparency — even a tiny bit of transparency, so tiny that the human eye cannot perceive it — and now clear will cut a hole in the view.
For example, your code works fine if you set the view's background color to UIColor.green.withAlphaComponent(0.99).
By the way, you should delete the lines about UIColor.clear; that's a red herring. You should also cut the lines about the backgroundColor; you should not be repainting the background color into your context. They are two different things.
The problem in the long run is that what you're doing is not how to punch a hole in a view. You should be using a mask instead. That's the only way you're going to get the animation while maintaining the hole.
Answering my own question, based on #matt's suggestion (and linked example), I did it with a CAShapeLayer. There was an extra "hitch" in my requirements, since I have a couple of views on top of the one I needed to mask out. So, I did the masking like this:
func cutOutWindow() {
// maskedBackgroundView is an additional view, inserted ONLY for the mask
let r = self.maskedBackgroundView.bounds
// Adjust frame for dragAreaView's border
var dragSize = self.dragAreaView.frame.size
var dragPosition = self.dragAreaView.frame.origin
dragSize.width -= 6.0
dragSize.height -= 6.0
dragPosition.x += 3.0
dragPosition.y += 3.0
let r2 = CGRect(x: dragPosition.x, y: dragPosition.y, width: dragSize.width, height: dragSize.height)
let roundedWindow = UIBezierPath(roundedRect: r2, cornerRadius: 15.0)
let mask = CAShapeLayer()
let path = CGMutablePath()
path.addPath(roundedWindow.cgPath)
path.addRect(r)
mask.path = path
mask.fillRule = kCAFillRuleEvenOdd
self.maskedBackgroundView.layer.mask = mask
}
Then I had to apply the colour change to maskedBackgroundView.layer.backgroundColor (i.e., to the layer, not the view). With that in place, I get the cutout I need, with animatable colour changes. Thanks #matt for pointing me in the right direction.

Add Gradient Layer to UIImageView in IOS thru CAGradientLayer

I am trying to add a Gradient in IOS like I did in Android. So I can see my label on top of the UIImageView and its not hidden.
In android I did this in a drawable `
<gradient
android:angle="90"
android:endColor="#00ffffff"
android:startColor="#aa000000"
android:centerColor="#00ffffff" />
<corners android:radius="0dp" />
`
I am trying to do this in IOS Swift 4.2 and I get the following :
let gradient: CAGradientLayer = CAGradientLayer()
gradient.colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
gradient.locations = [0.0 , 1.0]
gradient.startPoint = CGPoint(x: 0.0, y: 1.0)
gradient.endPoint = CGPoint(x: 0.5, y: 0.5)
gradient.frame = CGRect(x: 0.0, y: 0.0, width: showImageView.frame.size.width, height: showImageView.frame.size.height)
showImageView.layer.addSublayer(gradient)
How do I get the Gradient to start black from the bottom and go up in 90 degrees?How do I change the opacity?Any idea?
A few thoughts:
Choose colors with alpha less than 1. Perhaps:
gradient.colors = [
UIColor.black.withAlphaComponent(0.8).cgColor,
UIColor.black.withAlphaComponent(0).cgColor
]
To have this gradient go vertically, choose start and end points that have the same x value. E.g. to cover bottom half, perhaps:
gradient.startPoint = CGPoint(x: 0.5, y: 1)
gradient.endPoint = CGPoint(x: 0.5, y: 0.5)
Be very careful about using frame. You want the layer to reference the bounds of the image view (using the image view’s coordinate system), not the frame (which specifies where the image view is in its superview’s coordinate system). If your image view happens to be at 0, 0, you might not see a difference, but if you move the image view around at all, these will start to deviate. So, assuming you’re adding this gradient to the image view, itself, you’d use the image view’s bounds:
gradient.frame = showImageView.bounds
Be aware that the frame/bounds of the image view can sometimes change. So, we will implement layoutSubviews in our UIImageView or UITableViewCell subclass, and update the gradient’s frame there, e.g.
override func layoutSubviews() {
super.layoutSubviews()
gradient.frame = bounds
}
That way it will update the gradient’s frame when the view’s layout changes.
The other solution is to define a UIView subclass, say GradientView, that renders the CAGradientLayer and define the base layerClass of that view to be a CAGradientLayer. Then add this view in between the image view and the label, define its constraints, and then you’ll have a gradient that changes dynamically as the GradientView size changes. (This layerClass approach has the advantage that it yields better animation/transitions than you’d get by just updating the frame programmatically.)
Thus, something like:
class GradientView: UIView {
override class var layerClass: AnyClass { return CAGradientLayer.self }
var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
var firstColor: UIColor = UIColor.black.withAlphaComponent(0.8) {
didSet { updateColors() }
}
var secondColor: UIColor = UIColor.black.withAlphaComponent(0) {
didSet { updateColors() }
}
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
}
private extension GradientView {
func configure() {
updateColors()
gradientLayer.startPoint = CGPoint(x: 0.5, y: 1)
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0.5)
}
func updateColors() {
gradientLayer.colors = [firstColor, secondColor].map { $0.cgColor }
}
}
If you really want your white text to pop, in addition to adding gradient over the image, you might also add a black glow/shadow to the text. It’s subtle, but really makes the text pop. Just make its shadow color the same color as the gradient.
So you can see the effect, here are four renditions of a cell, with (a) no gradient; (b) with gradient; (c) with gradient and nice gaussian blur around text; and (d) with simple shadow around text:
The nice, gaussian blur around the text is rendered with:
customLabel.layer.shadowColor = UIColor.black.cgColor
customLabel.layer.shadowRadius = 3
customLabel.layer.shadowOpacity = 1
customLabel.layer.masksToBounds = false
customLabel.layer.shouldRasterize = true
I think that third rendition (with gradient over the image, with glow around the text) is best. It’s subtle, but the text really pops. But gaussian blurs are computationally expensive, so if you find this adversely affects your performance too much, you can use the fourth option, with the simple, non-blurred shadow:
customLabel.shadowColor = .black
// perhaps also
// customLabel.shadowOffset = CGSize(width: -1, height: -1)

iOS - How to apply gradient color to UICollectionViewCell backgroundView?

I have been able to apply the gradient I want to the backgroundView of my UICollectionViewCell. But every time I reload the cell, it applies again my gradient, accumulating all the gradient.
First of all here is my method for the gradient :
static func setGradientWhite(uiView: UIView) {
let colorBottomWhite = UIColor(red:1.00, green:1.00, blue:1.00, alpha:0.30).cgColor
let colorTopWhite = UIColor(red:1.00, green:1.00, blue:1.00, alpha:0).cgColor
let gradientLayer = CAGradientLayer()
gradientLayer.colors = [ colorTopWhite, colorBottomWhite]
gradientLayer.locations = [ 0.0, 1.0]
gradientLayer.frame = uiView.bounds
uiView.layer.insertSublayer(gradientLayer, at: 0)
}
I know I can do it in a more generic way, and change the way I am handling the colors, but it's not the point, it is for testing purpose for now.
I tried to call it like this :
override func awakeFromNib() {
super.awakeFromNib()
UIUtils.setGradientWhite(uiView: self)
}
The gradients do not accumulate this way, in the awakeFromNib, BUT, the animation isn't finish and I only have half of my cell with gradient applied.
And if I do it in this method :
override func layoutSubviews() {
super.layoutSubviews()
CATransaction.begin()
CATransaction.setDisableActions(true)
UIUtils.setGradientWhite(uiView: self)
CATransaction.commit()
}
The gradient animation is properly finished, applying to the whole view, but when the cell is reloading, it applies a new gradient on top of the previous one, until I cannot see my cell anymore (in this case too much white here).
Any ideas on how can I solve this issue?
You can try
override func layoutSubviews() {
super.layoutSubviews()
if !(self.layer.sublayers?.first is CAGradientLayer) {
CATransaction.begin()
CATransaction.setDisableActions(true)
UIUtils.setGradientWhite(uiView: self)
CATransaction.commit()
}
}

Button stretch when gradient applied

First I set constraints to a button in storyboard. Then, I edited it in code to make it look good.
When I apply my gradient function to a button it becomes bigger than before. How do you fix that?
extension UIView {
func applyGradient(colours: [UIColor]) -> Void {
self.applyGradient(colours, locations: nil)
}
func applyGradient(colours: [UIColor], locations: [NSNumber]?) -> Void {
let gradient: CAGradientLayer = CAGradientLayer()
gradient.frame = self.bounds
gradient.colors = colours.map { $0.CGColor }
gradient.locations = locations
self.layer.insertSublayer(gradient, atIndex: 0)
}
}
I just applied it on one button and..
Here is the pic...
Now after that it looks like this...The gradient bound did nothing It goes way outside frame PLZPLZPLZ try to help me :) I appreciate ur help so far
Add self.layer.masksToBounds = true to the function. This will mean that the sublayer you added won't draw outside of the original frame of the button
EDIT:
If you want the shadow as well you'll need to set self.layer.masksToBounds to false, since it will clip the shadow too.
Try gradient.frame = self.layer.bounds instead maybe?

How to tile an image from center using UIColor patternImage in Swift

I have a method for repeating or tiling an image across a view using UIColor patternImage:
view.backgroundColor = UIColor(patternImage: UIImage(named:imageName))
The default behaviour of UIColor patternImage is to start the pattern from the top left, the result shown in Image 1.
Question:
How can I get UIColor patternImage to start always from the very center of a view, patterning outwards to give the result shown in Image 2?
This works. It is a generalised method that applies more broadly, adapting dynamically to both pattern images and views of different heights and widths. It gives the desired result in the question and is tested on different iOS simulators.
view.backgroundColor = UIColor(patternImage: UIImage(named: "imageName")!)
view.bounds.origin.x = (UIImage(named: "imageName")!.size.width/2) - (view.bounds.size.width/2)
view.bounds.origin.y = (UIImage(named: "imageName")!.size.height/2) - (view.bounds.size.height/2)
If your image size is 50*50 then you can do something like this,
myView = UIVIew(frame: CGRect(x: (self.view.frame.size.width/2)-25, y: (self.view.frame.size.height/2)-25, width: 50, height: 50))
So this view(imageview in yourcase may be) will be place at exact middle of view. Like wise you can arrange other view by adding or substracting view's width and height to center view's x and y origin. Hope this will help you. :)
I created reusable class PatternView, reusing idea of #user4806509. It's really drop-in component for showing patterns. Also, patternImage can be set via Interface Builder
class PatternView: UIView {
#IBInspectable var patternImage: UIImage?
override func awakeFromNib() {
super.awakeFromNib()
self.setupBackgroundColor()
}
private func setupBackgroundColor() {
guard let patternImage = self.patternImage else {
return
}
self.backgroundColor = UIColor(patternImage: patternImage)
}
override func layoutSubviews() {
super.layoutSubviews()
self.centerPattern()
}
private func centerPattern() {
guard let patternSize = self.patternImage?.size,
patternSize.width > 0, patternSize.height > 0 else {
return
}
let x = -self.bounds.width.remainder(dividingBy: patternSize.width)/2
let y = -self.bounds.height.remainder(dividingBy: patternSize.height)/2
self.bounds.origin = CGPoint(x: x, y: y)
}}

Resources